-- DBAOps Enterprise™ - Create Stored Procedures
-- Generated: 2026-02-05 08:20:54
-- Procedures: 408
USE [DBAOpsRepository];
GO

-- [ai].[usp_CallAzureOpenAI]

CREATE PROCEDURE ai.usp_CallAzureOpenAI
    @UserPrompt NVARCHAR(MAX),
    @SystemPrompt NVARCHAR(MAX) = NULL,
    @CallingProcedure NVARCHAR(128) = NULL,
    @ServerContext NVARCHAR(256) = NULL,
    @RequestType NVARCHAR(50) = 'General',
    @Response NVARCHAR(MAX) OUTPUT
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @Endpoint NVARCHAR(500),
            @ApiKey NVARCHAR(500),
            @Deployment NVARCHAR(128),
            @ApiVersion NVARCHAR(50),
            @MaxTokens INT,
            @Temperature NVARCHAR(10),
            @TimeoutSeconds INT,
            @AIEnabled NVARCHAR(10),
            @BudgetLimit DECIMAL(10,2),
            @MonthToDateCost DECIMAL(10,2),
            @StartTime DATETIME2 = GETDATE(),
            @URL NVARCHAR(1000),
            @Headers NVARCHAR(1000),
            @Body NVARCHAR(MAX),
            @ResponseJSON NVARCHAR(MAX),
            @StatusCode INT,
            @ErrorMsg NVARCHAR(MAX);

    -- Load configuration
    SELECT @AIEnabled = ConfigValue FROM ai.Configuration WHERE ConfigKey = 'AI_Enabled';
    
    IF @AIEnabled != '1'
    BEGIN
        SET @Response = 'AI is currently disabled. Set AI_Enabled to 1 in ai.Configuration to activate.';
        RETURN;
    END

    SELECT @Endpoint = ConfigValue FROM ai.Configuration WHERE ConfigKey = 'AzureOpenAI_Endpoint';
    SELECT @ApiKey = ConfigValue FROM ai.Configuration WHERE ConfigKey = 'AzureOpenAI_ApiKey';
    SELECT @Deployment = ConfigValue FROM ai.Configuration WHERE ConfigKey = 'AzureOpenAI_Deployment';
    SELECT @ApiVersion = ConfigValue FROM ai.Configuration WHERE ConfigKey = 'AzureOpenAI_ApiVersion';
    SELECT @MaxTokens = CAST(ConfigValue AS INT) FROM ai.Configuration WHERE ConfigKey = 'AzureOpenAI_MaxTokens';
    SELECT @Temperature = ConfigValue FROM ai.Configuration WHERE ConfigKey = 'AzureOpenAI_Temperature';
    SELECT @TimeoutSeconds = CAST(ConfigValue AS INT) FROM ai.Configuration WHERE ConfigKey = 'AzureOpenAI_TimeoutSeconds';
    SELECT @BudgetLimit = CAST(ConfigValue AS DECIMAL(10,2)) FROM ai.Configuration WHERE ConfigKey = 'AI_MonthlyBudgetLimit';

    -- Check if endpoint is placeholder
    IF @Endpoint LIKE '%your-resource%' OR @ApiKey = 'PENDING_AZURE_APPROVAL'
    BEGIN
        SET @Response = 'Azure OpenAI credentials not yet configured. Run ai.usp_SetAICredential after receiving Azure access.';
        RETURN;
    END

    -- Budget check
    SELECT @MonthToDateCost = ISNULL(SUM(EstimatedCostUSD), 0)
    FROM ai.APIUsageLog
    WHERE MONTH(RequestDate) = MONTH(GETDATE()) AND YEAR(RequestDate) = YEAR(GETDATE());

    IF @MonthToDateCost >= @BudgetLimit
    BEGIN
        SET @Response = 'Monthly AI budget limit of $' + CAST(@BudgetLimit AS VARCHAR) + ' has been reached. Current spend: $' + CAST(@MonthToDateCost AS VARCHAR);
        
        INSERT INTO ai.APIUsageLog (CallingProcedure, StatusCode, ErrorMessage, ServerContext, RequestType)
        VALUES (@CallingProcedure, 429, 'Budget limit reached', @ServerContext, @RequestType);
        RETURN;
    END

    -- Use system prompt from config if not provided
    IF @SystemPrompt IS NULL
        SELECT @SystemPrompt = ConfigValue FROM ai.Configuration WHERE ConfigKey = 'AzureOpenAI_SystemPrompt';

    -- Build the API URL
    SET @URL = @Endpoint + 'openai/deployments/' + @Deployment + '/chat/completions?api-version=' + @ApiVersion;

    -- Build request body (escape JSON properly)
    SET @Body = '{
        "messages": [
            {"role": "system", "content": "' + REPLACE(REPLACE(@SystemPrompt, '"', '\"'), CHAR(10), '\n') + '"},
            {"role": "user", "content": "' + REPLACE(REPLACE(@UserPrompt, '"', '\"'), CHAR(10), '\n') + '"}
        ],
        "max_tokens": ' + CAST(@MaxTokens AS VARCHAR) + ',
        "temperature": ' + @Temperature + '
    }';

    -- NOTE: The actual HTTP call will use one of these methods:
    -- Option A: sp_invoke_external_rest_endpoint (Azure SQL only)
    -- Option B: PowerShell via xp_cmdshell (on-premises)
    -- Option C: CLR stored procedure
    -- Option D: SQL Agent job with PowerShell step
    --
    -- For on-premises SQL Server, we will use Option B (PowerShell via xp_cmdshell)
    -- This will be configured when Azure credentials are available

    SET @Response = 'API call infrastructure ready. Awaiting Azure OpenAI credentials. URL template: ' + @URL;

    -- Log the attempt
    INSERT INTO ai.APIUsageLog (CallingProcedure, StatusCode, ErrorMessage, ServerContext, RequestType, ResponseTimeMS)
    VALUES (@CallingProcedure, 0, 'Dry run: credentials pending', @ServerContext, @RequestType, 
            DATEDIFF(MILLISECOND, @StartTime, GETDATE()));

    RETURN;
END

GO

-- [ai].[usp_DetectAnomalies]

CREATE   PROCEDURE AI.usp_DetectAnomalies
    @LookbackHours INT = 24,
    @DeviationThreshold DECIMAL(5,2) = 2.0 -- Standard deviations
AS
BEGIN
    SET NOCOUNT ON;
    
    PRINT 'DBAOps AI Anomaly Detection';
    PRINT 'Lookback: ' + CAST(@LookbackHours AS VARCHAR) + ' hours';
    PRINT 'Threshold: ' + CAST(@DeviationThreshold AS VARCHAR) + ' standard deviations';
    PRINT '';
    
    -- Only process servers in CMS (source of truth)
    DECLARE @CMSServers TABLE (ServerName NVARCHAR(128));
    INSERT INTO @CMSServers
    SELECT ServerName FROM config.vw_MonitoredServers WHERE IsActive = 1;
    
    -- Detect CPU anomalies using CPU_Utilization column
    ;WITH CPUStats AS (
        SELECT 
            f.ServerName,
            AVG(CAST(f.CPU_Utilization AS DECIMAL(10,2))) AS AvgCPU,
            STDEV(CAST(f.CPU_Utilization AS DECIMAL(10,2))) AS StdDevCPU,
            MAX(CAST(f.CPU_Utilization AS DECIMAL(10,2))) AS MaxCPU
        FROM dbo.FACT_ServerHealth f
        INNER JOIN @CMSServers cms ON f.ServerName = cms.ServerName
        WHERE f.CaptureTime >= DATEADD(HOUR, -@LookbackHours * 7, GETDATE()) -- Baseline from past week
        AND f.CPU_Utilization IS NOT NULL
        GROUP BY f.ServerName
    ),
    RecentCPU AS (
        SELECT 
            f.ServerName,
            AVG(CAST(f.CPU_Utilization AS DECIMAL(10,2))) AS RecentAvgCPU
        FROM dbo.FACT_ServerHealth f
        INNER JOIN @CMSServers cms ON f.ServerName = cms.ServerName
        WHERE f.CaptureTime >= DATEADD(HOUR, -@LookbackHours, GETDATE())
        AND f.CPU_Utilization IS NOT NULL
        GROUP BY f.ServerName
    )
    INSERT INTO AI.AnomalyDetections (ServerName, MetricName, ExpectedValue, ActualValue, DeviationPercent, AnomalyScore, AIAnalysis)
    SELECT 
        s.ServerName,
        'CPU_Utilization',
        s.AvgCPU,
        r.RecentAvgCPU,
        CASE WHEN s.AvgCPU > 0 THEN ((r.RecentAvgCPU - s.AvgCPU) / s.AvgCPU) * 100 ELSE 0 END,
        CASE WHEN s.StdDevCPU > 0 THEN ABS(r.RecentAvgCPU - s.AvgCPU) / s.StdDevCPU * 10 ELSE 0 END,
        'CPU usage significantly different from baseline. Investigate running queries and processes.'
    FROM CPUStats s
    JOIN RecentCPU r ON s.ServerName = r.ServerName
    WHERE s.StdDevCPU > 0 
    AND ABS(r.RecentAvgCPU - s.AvgCPU) > (s.StdDevCPU * @DeviationThreshold);
    
    DECLARE @CPUAnomalyCount INT = @@ROWCOUNT;
    
    -- Detect Memory (PLE) anomalies
    ;WITH PLEStats AS (
        SELECT 
            f.ServerName,
            AVG(CAST(f.Memory_PLE AS DECIMAL(10,2))) AS AvgPLE,
            STDEV(CAST(f.Memory_PLE AS DECIMAL(10,2))) AS StdDevPLE
        FROM dbo.FACT_ServerHealth f
        INNER JOIN @CMSServers cms ON f.ServerName = cms.ServerName
        WHERE f.CaptureTime >= DATEADD(HOUR, -@LookbackHours * 7, GETDATE())
        AND f.Memory_PLE IS NOT NULL
        GROUP BY f.ServerName
    ),
    RecentPLE AS (
        SELECT 
            f.ServerName,
            AVG(CAST(f.Memory_PLE AS DECIMAL(10,2))) AS RecentAvgPLE
        FROM dbo.FACT_ServerHealth f
        INNER JOIN @CMSServers cms ON f.ServerName = cms.ServerName
        WHERE f.CaptureTime >= DATEADD(HOUR, -@LookbackHours, GETDATE())
        AND f.Memory_PLE IS NOT NULL
        GROUP BY f.ServerName
    )
    INSERT INTO AI.AnomalyDetections (ServerName, MetricName, ExpectedValue, ActualValue, DeviationPercent, AnomalyScore, AIAnalysis)
    SELECT 
        s.ServerName,
        'Memory_PLE',
        s.AvgPLE,
        r.RecentAvgPLE,
        CASE WHEN s.AvgPLE > 0 THEN ((r.RecentAvgPLE - s.AvgPLE) / s.AvgPLE) * 100 ELSE 0 END,
        CASE WHEN s.StdDevPLE > 0 THEN ABS(r.RecentAvgPLE - s.AvgPLE) / s.StdDevPLE * 10 ELSE 0 END,
        'Page Life Expectancy significantly different from baseline. Check for memory pressure or large queries.'
    FROM PLEStats s
    JOIN RecentPLE r ON s.ServerName = r.ServerName
    WHERE s.StdDevPLE > 0 
    AND ABS(r.RecentAvgPLE - s.AvgPLE) > (s.StdDevPLE * @DeviationThreshold);
    
    DECLARE @PLEAnomalyCount INT = @@ROWCOUNT;
    
    -- Return detected anomalies
    SELECT 
        ServerName,
        MetricName,
        CAST(ExpectedValue AS DECIMAL(10,2)) AS ExpectedValue,
        CAST(ActualValue AS DECIMAL(10,2)) AS ActualValue,
        CAST(DeviationPercent AS DECIMAL(10,2)) AS DeviationPercent,
        CAST(AnomalyScore AS DECIMAL(5,2)) AS AnomalyScore,
        AIAnalysis,
        DetectedDate
    FROM AI.AnomalyDetections
    WHERE DetectedDate >= DATEADD(HOUR, -1, GETDATE())
    ORDER BY AnomalyScore DESC;
    
    PRINT '';
    PRINT 'CPU Anomalies: ' + CAST(@CPUAnomalyCount AS VARCHAR);
    PRINT 'PLE Anomalies: ' + CAST(@PLEAnomalyCount AS VARCHAR);
    PRINT 'Total Anomalies: ' + CAST(@CPUAnomalyCount + @PLEAnomalyCount AS VARCHAR);
END;

GO

-- [ai].[usp_GetRemediationScript]

CREATE   PROCEDURE AI.usp_GetRemediationScript
    @Category NVARCHAR(50),
    @IssueType NVARCHAR(100),
    @ServerName NVARCHAR(128) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @Script NVARCHAR(MAX);
    DECLARE @Description NVARCHAR(MAX);
    
    -- Pre-built remediation scripts based on common issues
    SELECT @Script = CASE
        -- HIGH CPU
        WHEN @Category = 'Performance' AND @IssueType LIKE '%cpu%'
            THEN '-- Check for expensive queries
SELECT TOP 10 
    qs.total_worker_time/1000000 AS TotalCPU_Seconds,
    qs.execution_count,
    qs.total_worker_time/qs.execution_count/1000 AS AvgCPU_ms,
    SUBSTRING(st.text, (qs.statement_start_offset/2)+1,
        ((CASE qs.statement_end_offset WHEN -1 THEN DATALENGTH(st.text)
        ELSE qs.statement_end_offset END - qs.statement_start_offset)/2)+1) AS QueryText
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st
ORDER BY qs.total_worker_time DESC;

-- Check for missing indexes
SELECT TOP 10
    ROUND(s.avg_total_user_cost * s.avg_user_impact * (s.user_seeks + s.user_scans),0) AS [Total Cost],
    d.[statement] AS [Table],
    d.equality_columns,
    d.inequality_columns,
    d.included_columns
FROM sys.dm_db_missing_index_groups g
JOIN sys.dm_db_missing_index_group_stats s ON s.group_handle = g.index_group_handle
JOIN sys.dm_db_missing_index_details d ON d.index_handle = g.index_handle
ORDER BY [Total Cost] DESC;'

        -- LOW PLE
        WHEN @Category = 'Performance' AND @IssueType LIKE '%ple%' OR @IssueType LIKE '%memory%'
            THEN '-- Check memory configuration
SELECT 
    physical_memory_kb/1024 AS Physical_Memory_MB,
    committed_kb/1024 AS Committed_MB,
    committed_target_kb/1024 AS Target_MB
FROM sys.dm_os_sys_info;

-- Check for memory pressure
SELECT 
    type, 
    SUM(pages_kb)/1024 AS MB
FROM sys.dm_os_memory_clerks
GROUP BY type
ORDER BY SUM(pages_kb) DESC;

-- Consider increasing Max Server Memory or adding RAM'

        -- BLOCKING
        WHEN @Category = 'Performance' AND @IssueType LIKE '%block%'
            THEN '-- Find blocking chains
SELECT 
    r.session_id AS BlockedSession,
    r.blocking_session_id AS BlockingSession,
    r.wait_type,
    r.wait_time/1000 AS WaitSeconds,
    st.text AS BlockedQuery
FROM sys.dm_exec_requests r
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) st
WHERE r.blocking_session_id > 0;

-- Kill blocking session (use with caution!)
-- KILL <blocking_session_id>;'

        -- DISK SPACE
        WHEN @Category = 'Disk' AND @IssueType LIKE '%space%'
            THEN '-- Check database file sizes
SELECT 
    DB_NAME(database_id) AS DatabaseName,
    name AS FileName,
    type_desc,
    size * 8 / 1024 AS SizeMB,
    FILEPROPERTY(name, ''SpaceUsed'') * 8 / 1024 AS UsedMB
FROM sys.master_files
ORDER BY size DESC;

-- Shrink log file (after backup)
-- USE [DatabaseName]; DBCC SHRINKFILE (LogFileName, 1024);

-- Check for large tables
SELECT TOP 10
    OBJECT_NAME(object_id) AS TableName,
    SUM(reserved_page_count) * 8 / 1024 AS SizeMB
FROM sys.dm_db_partition_stats
GROUP BY object_id
ORDER BY SUM(reserved_page_count) DESC;'

        -- BACKUP FAILURE
        WHEN @Category = 'Backup' AND @IssueType LIKE '%fail%'
            THEN '-- Check backup history
SELECT TOP 10
    database_name,
    backup_start_date,
    backup_finish_date,
    type,
    CASE type 
        WHEN ''D'' THEN ''Full''
        WHEN ''I'' THEN ''Differential''
        WHEN ''L'' THEN ''Log''
    END AS BackupType
FROM msdb.dbo.backupset
ORDER BY backup_start_date DESC;

-- Run backup now
-- BACKUP DATABASE [DatabaseName] TO DISK = ''Path'' WITH COMPRESSION, CHECKSUM;

-- Check for backup job errors
SELECT TOP 10 
    j.name, 
    h.run_date, 
    h.run_time,
    h.message
FROM msdb.dbo.sysjobhistory h
JOIN msdb.dbo.sysjobs j ON h.job_id = j.job_id
WHERE j.name LIKE ''%backup%''
AND h.run_status = 0
ORDER BY h.run_date DESC, h.run_time DESC;'

        -- MAXDOP NON-COMPLIANT
        WHEN @Category = 'Config' AND @IssueType LIKE '%maxdop%'
            THEN '-- Check current MAXDOP
SELECT name, value, value_in_use 
FROM sys.configurations 
WHERE name = ''max degree of parallelism'';

-- Recommended MAXDOP based on CPU count
-- For OLTP: 1-8 (typically 4)
-- For OLAP/DW: 0 or half of CPU count

-- Set MAXDOP (example: 4)
-- EXEC sp_configure ''max degree of parallelism'', 4;
-- RECONFIGURE;'

        ELSE '-- No specific remediation script available for: ' + @Category + ' / ' + @IssueType + '
-- Consult DBA documentation or use AI.usp_RunDiagnostic for detailed analysis.'
    END;
    
    SELECT 
        @Category AS Category,
        @IssueType AS IssueType,
        @ServerName AS TargetServer,
        @Script AS RemediationScript,
        'Review script before executing. Test in non-production first.' AS Warning;
END;

GO

-- [ai].[usp_LogDiagnostic]

CREATE PROCEDURE ai.usp_LogDiagnostic
    @ServerName NVARCHAR(128),
    @DiagnosticType NVARCHAR(50),
    @IssueSummary NVARCHAR(500),
    @AIAnalysis NVARCHAR(MAX) = NULL,
    @RemediationSteps NVARCHAR(MAX) = NULL,
    @Severity INT = 2,
    @AIConfidence DECIMAL(5,2) = NULL,
    @RelatedAlertID BIGINT = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    INSERT INTO ai.fact_Diagnostics
    (
        ServerName,
        DiagnosticType,
        IssueSummary,
        AIAnalysis,
        RemediationSteps,
        Severity,
        AIConfidence,
        RelatedAlertID,
        DetectedDate,
        Status
    )
    VALUES
    (
        @ServerName,
        @DiagnosticType,
        @IssueSummary,
        @AIAnalysis,
        @RemediationSteps,
        @Severity,
        @AIConfidence,
        @RelatedAlertID,
        GETDATE(),
        'Open'
    );
    
    SELECT 
        SCOPE_IDENTITY() AS DiagnosticID,
        'Diagnostic logged successfully' AS Message;
END;

GO

-- [ai].[usp_NaturalLanguageSearch]

CREATE   PROCEDURE AI.usp_NaturalLanguageSearch
    @Query NVARCHAR(MAX),
    @MaxRows INT = 50,
    @Debug BIT = 0
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @GeneratedSQL NVARCHAR(MAX);
    DECLARE @StartTime DATETIME2 = SYSDATETIME();
    DECLARE @MaxRowsStr VARCHAR(10) = CAST(@MaxRows AS VARCHAR);
    DECLARE @QueryLower NVARCHAR(MAX) = LOWER(@Query);
    
    SET @GeneratedSQL = CASE
        -- ====================================================================
        -- DISK SPACE QUERIES (Deduplicated)
        -- ====================================================================
        WHEN @QueryLower LIKE '%disk%space%' OR @QueryLower LIKE '%low%disk%' OR @QueryLower LIKE '%disk%low%' OR @QueryLower LIKE '%disk%issue%'
            THEN 'SELECT TOP ' + @MaxRowsStr + ' ServerName, DriveLetter, VolumeName, TotalSizeGB, FreeSpaceGB, FreeSpacePercent, DashboardColor, AlertLevel, CMSGroup FROM dashboard.vw_DiskSpaceMonitor WHERE AlertLevel IN (''Critical'', ''Warning'', ''Caution'') ORDER BY FreeSpacePercent'
        
        WHEN @QueryLower LIKE '%disk%monitor%' OR @QueryLower LIKE '%all%disk%'
            THEN 'SELECT TOP ' + @MaxRowsStr + ' ServerName, DriveLetter, VolumeName, TotalSizeGB, FreeSpaceGB, FreeSpacePercent, DashboardColor, AlertLevel, CMSGroup FROM dashboard.vw_DiskSpaceMonitor ORDER BY FreeSpacePercent'
        
        WHEN @QueryLower LIKE '%disk%critical%' OR @QueryLower LIKE '%disk%red%'
            THEN 'SELECT TOP ' + @MaxRowsStr + ' ServerName, DriveLetter, VolumeName, TotalSizeGB, FreeSpaceGB, FreeSpacePercent, DashboardColor, AlertLevel, CMSGroup FROM dashboard.vw_DiskSpaceMonitor WHERE AlertLevel = ''Critical'' ORDER BY FreeSpacePercent'
        
        WHEN @QueryLower LIKE '%disk%warning%' OR @QueryLower LIKE '%disk%orange%'
            THEN 'SELECT TOP ' + @MaxRowsStr + ' ServerName, DriveLetter, VolumeName, TotalSizeGB, FreeSpaceGB, FreeSpacePercent, DashboardColor, AlertLevel, CMSGroup FROM dashboard.vw_DiskSpaceMonitor WHERE AlertLevel = ''Warning'' ORDER BY FreeSpacePercent'
        
        WHEN @QueryLower LIKE '%disk%summary%' OR @QueryLower LIKE '%disk%metric%'
            THEN 'SELECT * FROM dashboard.vw_DiskSpaceSummary'
        
        -- ====================================================================
        -- REMEDIATION QUERIES
        -- ====================================================================
        WHEN @QueryLower LIKE '%remediat%' OR @QueryLower LIKE '%fix%issue%' OR @QueryLower LIKE '%how%to%fix%'
            THEN 'EXEC dashboard.usp_SearchRemediation @Category = ''All'', @MaxResults = ' + @MaxRowsStr
        
        WHEN @QueryLower LIKE '%remediat%disk%' OR @QueryLower LIKE '%fix%disk%'
            THEN 'EXEC dashboard.usp_SearchRemediation @Category = ''Disk'', @MaxResults = ' + @MaxRowsStr
        
        WHEN @QueryLower LIKE '%remediat%patch%' OR @QueryLower LIKE '%fix%patch%'
            THEN 'EXEC dashboard.usp_SearchRemediation @Category = ''Patch'', @MaxResults = ' + @MaxRowsStr
        
        WHEN @QueryLower LIKE '%remediat%backup%' OR @QueryLower LIKE '%fix%backup%'
            THEN 'EXEC dashboard.usp_SearchRemediation @Category = ''Backup'', @MaxResults = ' + @MaxRowsStr
        
        WHEN @QueryLower LIKE '%remediat%performance%' OR @QueryLower LIKE '%fix%performance%' OR @QueryLower LIKE '%fix%cpu%' OR @QueryLower LIKE '%fix%memory%'
            THEN 'EXEC dashboard.usp_SearchRemediation @Category = ''Performance'', @MaxResults = ' + @MaxRowsStr
        
        -- ====================================================================
        -- COMPLIANCE QUERIES
        -- ====================================================================
        WHEN @QueryLower LIKE '%compliance%framework%' OR @QueryLower LIKE '%government%compliance%' OR @QueryLower LIKE '%fedramp%' OR @QueryLower LIKE '%cjis%' OR @QueryLower LIKE '%dod%il%'
            THEN 'SELECT ComplianceName, ComplianceCode, Description, AppliesTo, AuthorizationLevel FROM config.vw_GovernmentCompliance ORDER BY SortOrder'
        
        -- ====================================================================
        -- HELP DESK QUERIES
        -- ====================================================================
        WHEN @QueryLower LIKE '%help%desk%' OR @QueryLower LIKE '%helpdesk%'
            THEN 'SELECT TOP ' + @MaxRowsStr + ' ServerName, CMSGroup, Environment, TierLevel, OverallStatus, DashboardColor, DiskAlertCount, PatchStatus, ComplianceFrameworks FROM dashboard.vw_HelpDeskStatus ORDER BY GroupPriority, OverallStatus'
        
        WHEN @QueryLower LIKE '%help%desk%critical%' OR @QueryLower LIKE '%helpdesk%critical%'
            THEN 'SELECT TOP ' + @MaxRowsStr + ' ServerName, CMSGroup, Environment, TierLevel, OverallStatus, DashboardColor, DiskAlertCount, PatchStatus, ComplianceFrameworks FROM dashboard.vw_HelpDeskStatus WHERE OverallStatus = ''Critical'' ORDER BY GroupPriority'
        
        -- ====================================================================
        -- METRICS/DASHBOARD QUERIES
        -- ====================================================================
        WHEN @QueryLower LIKE '%metric%' OR @QueryLower LIKE '%infrastructure%summary%' OR @QueryLower LIKE '%dashboard%metric%'
            THEN 'SELECT * FROM dashboard.vw_InfrastructureMetrics'
        
        -- ====================================================================
        -- SERVER HEALTH QUERIES
        -- ====================================================================
        WHEN @QueryLower LIKE '%server%need%attention%' OR @QueryLower LIKE '%problem%server%'
            THEN 'SELECT TOP ' + @MaxRowsStr + ' * FROM dashboard.vw_ServersNeedingAttention ORDER BY Priority'
        
        WHEN @QueryLower LIKE '%server%health%summary%' OR @QueryLower LIKE '%health%summary%'
            THEN 'SELECT TOP ' + @MaxRowsStr + ' * FROM dashboard.vw_ServerHealthSummary ORDER BY OverallHealth'
        
        WHEN @QueryLower LIKE '%high%cpu%' OR @QueryLower LIKE '%cpu%high%' OR @QueryLower LIKE '%cpu%issue%'
            THEN 'SELECT TOP ' + @MaxRowsStr + ' ServerName, CaptureTime, CPU_Utilization, Memory_PLE, BlockingSessions FROM dbo.FACT_ServerHealth WHERE CPU_Utilization > 80 ORDER BY CaptureTime DESC'
        
        WHEN @QueryLower LIKE '%low%memory%' OR @QueryLower LIKE '%memory%pressure%' OR @QueryLower LIKE '%ple%'
            THEN 'SELECT TOP ' + @MaxRowsStr + ' ServerName, CaptureTime, Memory_PLE, Memory_TotalMB, Memory_UsedMB FROM dbo.FACT_ServerHealth WHERE Memory_PLE < 300 AND Memory_PLE IS NOT NULL ORDER BY CaptureTime DESC'
        
        WHEN @QueryLower LIKE '%blocking%' OR @QueryLower LIKE '%blocked%'
            THEN 'SELECT TOP ' + @MaxRowsStr + ' ServerName, CaptureTime, BlockingSessions, ActiveSessions FROM dbo.FACT_ServerHealth WHERE BlockingSessions > 0 ORDER BY CaptureTime DESC'
        
        -- ====================================================================
        -- BACKUP QUERIES
        -- ====================================================================
        WHEN @QueryLower LIKE '%backup%fail%' OR @QueryLower LIKE '%fail%backup%' OR @QueryLower LIKE '%backup%issue%'
            THEN 'SELECT TOP ' + @MaxRowsStr + ' ServerName, DatabaseName, Status, IsCompliant, FullAgeHours, LogAgeMinutes, LastBackupTime FROM ctl.BackupCompliance WHERE IsCompliant = 0 ORDER BY FullAgeHours DESC'
        
        WHEN @QueryLower LIKE '%backup%compliance%' OR @QueryLower LIKE '%backup%status%' OR @QueryLower LIKE '%backup%summary%'
            THEN 'SELECT TOP ' + @MaxRowsStr + ' * FROM dbo.vw_BackupCompliance_Summary'
        
        -- ====================================================================
        -- PATCHING QUERIES
        -- ====================================================================
        WHEN @QueryLower LIKE '%patch%' AND (@QueryLower LIKE '%need%' OR @QueryLower LIKE '%missing%')
            THEN 'SELECT TOP ' + @MaxRowsStr + ' * FROM Patching.vw_ServersNeedingPatches'
        
        WHEN @QueryLower LIKE '%patch%detail%'
            THEN 'SELECT TOP ' + @MaxRowsStr + ' ServerName, ComplianceStatus, MissingPatches, MissingCriticalPatches, DaysSinceLastPatch, NonComplianceReason FROM Patching.TaniumPatchStatus WHERE IsActive = 1 ORDER BY MissingPatches DESC'
        
        WHEN @QueryLower LIKE '%non%compliant%patch%' OR @QueryLower LIKE '%patch%non%compliant%'
            THEN 'SELECT TOP ' + @MaxRowsStr + ' ServerName, ComplianceStatus, MissingPatches, DaysSinceLastPatch, NonComplianceReason FROM Patching.TaniumPatchStatus WHERE ComplianceStatus = ''Non-Compliant'' AND IsActive = 1'
        
        -- ====================================================================
        -- EOS / END OF SUPPORT QUERIES
        -- ====================================================================
        WHEN @QueryLower LIKE '%end%of%support%' OR @QueryLower LIKE '%eos%' OR @QueryLower LIKE '%out%of%support%'
            THEN 'SELECT TOP ' + @MaxRowsStr + ' * FROM dashboard.vw_EOS_ServerList'
        
        -- ====================================================================
        -- SECURITY QUERIES
        -- ====================================================================
        WHEN @QueryLower LIKE '%sysadmin%' OR @QueryLower LIKE '%admin%login%'
            THEN 'SELECT TOP ' + @MaxRowsStr + ' * FROM security.LoginInventory WHERE IsSysAdmin = 1 ORDER BY ServerName'
        
        WHEN @QueryLower LIKE '%permission%drift%' OR @QueryLower LIKE '%permission%change%'
            THEN 'SELECT TOP ' + @MaxRowsStr + ' * FROM security.PermissionDrift WHERE IsResolved = 0 ORDER BY DetectedDate DESC'
        
        -- ====================================================================
        -- SERVER INVENTORY QUERIES (CMS Source of Truth)
        -- ====================================================================
        WHEN @QueryLower LIKE '%high%criticality%' OR @QueryLower LIKE '%critical%server%' OR @QueryLower LIKE '%tier%1%'
            THEN 'SELECT TOP ' + @MaxRowsStr + ' * FROM config.vw_MonitoredServers WHERE GroupName = ''HIGH_CRITICALITY'' AND IsActive = 1'
        
        WHEN @QueryLower LIKE '%medium%criticality%' OR @QueryLower LIKE '%tier%2%'
            THEN 'SELECT TOP ' + @MaxRowsStr + ' * FROM config.vw_MonitoredServers WHERE GroupName = ''MEDIUM_CRITICALITY'' AND IsActive = 1'
        
        WHEN @QueryLower LIKE '%nonprod%' OR @QueryLower LIKE '%non-prod%' OR @QueryLower LIKE '%test%server%' OR @QueryLower LIKE '%tier%4%'
            THEN 'SELECT TOP ' + @MaxRowsStr + ' * FROM config.vw_MonitoredServers WHERE GroupName = ''NONPROD_APP'' AND IsActive = 1'
        
        WHEN @QueryLower LIKE '%cms%' OR @QueryLower LIKE '%all%server%' OR @QueryLower LIKE '%server%list%' OR @QueryLower LIKE '%server%inventory%'
            THEN 'SELECT TOP ' + @MaxRowsStr + ' * FROM config.vw_MonitoredServers WHERE IsActive = 1 ORDER BY GroupName, ServerName'
        
        WHEN @QueryLower LIKE '%server%count%' OR @QueryLower LIKE '%how%many%server%'
            THEN 'SELECT GroupName, COUNT(*) AS ServerCount FROM config.vw_MonitoredServers WHERE IsActive = 1 GROUP BY GroupName ORDER BY ServerCount DESC'
        
        -- ====================================================================
        -- EXECUTIVE / KPI QUERIES
        -- ====================================================================
        WHEN @QueryLower LIKE '%executive%' OR @QueryLower LIKE '%kpi%' OR @QueryLower LIKE '%dashboard%summary%'
            THEN 'SELECT * FROM pbi.ExecutiveKPISummary'
        
        ELSE NULL
    END;
    
    -- Log the search
    INSERT INTO AI.SearchHistory (NaturalLanguageQuery, GeneratedSQL, SearchedBy)
    VALUES (@Query, @GeneratedSQL, SUSER_NAME());
    
    IF @GeneratedSQL IS NULL
    BEGIN
        SELECT 
            'Query not understood. Try:' AS Message,
            'show low disk space | show disk critical | show disk summary' AS DiskExamples,
            'show remediation | fix disk issues | fix patch issues' AS RemediationExamples,
            'show compliance frameworks | show government compliance' AS ComplianceExamples,
            'show help desk | show help desk critical' AS HelpDeskExamples,
            'show metrics | show infrastructure summary' AS MetricExamples,
            'show servers needing attention | show high cpu' AS HealthExamples;
        RETURN;
    END
    
    IF @Debug = 1
        PRINT 'Generated: ' + @GeneratedSQL;
    
    EXEC sp_executesql @GeneratedSQL;
    
    UPDATE AI.SearchHistory 
    SET ExecutionTimeMs = DATEDIFF(MILLISECOND, @StartTime, SYSDATETIME())
    WHERE SearchID = SCOPE_IDENTITY();
END;

GO

-- [ai].[usp_RunDiagnostic]

CREATE   PROCEDURE AI.usp_RunDiagnostic
    @ServerName NVARCHAR(128) = NULL,
    @Category NVARCHAR(50) = 'All', -- 'Health', 'Backup', 'Disk', 'Performance', 'Security', 'Patching', 'All'
    @IncludeRemediation BIT = 1,
    @Debug BIT = 0
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @StartTime DATETIME2 = SYSDATETIME();
    DECLARE @Context NVARCHAR(MAX);
    DECLARE @Prompt NVARCHAR(MAX);
    DECLARE @Issues NVARCHAR(MAX) = '';
    
    PRINT 'DBAOps AI Diagnostic Starting...';
    PRINT 'Server: ' + ISNULL(@ServerName, 'All Servers');
    PRINT 'Category: ' + @Category;
    PRINT '';
    
    -- Validate server exists in CMS (source of truth)
    IF @ServerName IS NOT NULL
    BEGIN
        IF NOT EXISTS (SELECT 1 FROM config.vw_MonitoredServers WHERE ServerName = @ServerName AND IsActive = 1)
        BEGIN
            PRINT 'WARNING: Server not found in CMS or not active: ' + @ServerName;
        END
    END
    
    -- ========================================================================
    -- HEALTH ISSUES - Using dashboard.vw_ServersNeedingAttention
    -- Columns: ServerName, Environment, AppCategory, SQLVersion, LastSeen, Notes, Issue, Priority
    -- ========================================================================
    IF @Category IN ('Health', 'All')
    BEGIN
        SELECT @Issues = @Issues + 
            'HEALTH ISSUE: Server ' + ServerName + 
            ' [' + ISNULL(Environment, 'Unknown') + '/' + ISNULL(AppCategory, 'Unknown') + ']' +
            ' - Issue: ' + ISNULL(Issue, 'Unknown') +
            ' (Priority: ' + CAST(ISNULL(Priority, 0) AS VARCHAR) + ')' + CHAR(13)
        FROM dashboard.vw_ServersNeedingAttention
        WHERE (@ServerName IS NULL OR ServerName = @ServerName);
        
        -- Also check ServerHealthSummary for critical alerts
        SELECT @Issues = @Issues +
            'CRITICAL ALERT: Server ' + ServerName +
            ' - OverallHealth: ' + ISNULL(OverallHealth, 'Unknown') +
            ', ActiveAlerts: ' + CAST(ISNULL(ActiveAlertCount, 0) AS VARCHAR) +
            ', BuildsBehind: ' + CAST(ISNULL(BuildsBehind, 0) AS VARCHAR) + CHAR(13)
        FROM dashboard.vw_ServerHealthSummary
        WHERE HasCriticalAlert = 1 OR HasCriticalDiagnostic = 1
        AND (@ServerName IS NULL OR ServerName = @ServerName);
    END
    
    -- ========================================================================
    -- PERFORMANCE ISSUES - Using dbo.FACT_ServerHealth
    -- Columns: CPU_Utilization, Memory_PLE, BlockingSessions, Memory_PressureFlag, CPU_PressureFlag
    -- ========================================================================
    IF @Category IN ('Performance', 'All')
    BEGIN
        -- High CPU (>80%)
        SELECT @Issues = @Issues + 
            'HIGH CPU: Server ' + ServerName + 
            ' - CPU: ' + CAST(CAST(CPU_Utilization AS DECIMAL(5,2)) AS VARCHAR) + '%' +
            ' at ' + CONVERT(VARCHAR(20), CaptureTime, 120) + CHAR(13)
        FROM dbo.FACT_ServerHealth
        WHERE CPU_Utilization > 80
        AND CaptureTime >= DATEADD(HOUR, -24, GETDATE())
        AND (@ServerName IS NULL OR ServerName = @ServerName);
        
        -- Low PLE (<300)
        SELECT @Issues = @Issues + 
            'LOW MEMORY (PLE): Server ' + ServerName + 
            ' - PLE: ' + CAST(ISNULL(Memory_PLE, 0) AS VARCHAR) +
            ' at ' + CONVERT(VARCHAR(20), CaptureTime, 120) + CHAR(13)
        FROM dbo.FACT_ServerHealth
        WHERE Memory_PLE < 300 AND Memory_PLE IS NOT NULL
        AND CaptureTime >= DATEADD(HOUR, -24, GETDATE())
        AND (@ServerName IS NULL OR ServerName = @ServerName);
        
        -- Blocking
        SELECT @Issues = @Issues + 
            'BLOCKING: Server ' + ServerName + 
            ' - Blocking Sessions: ' + CAST(ISNULL(BlockingSessions, 0) AS VARCHAR) +
            ' at ' + CONVERT(VARCHAR(20), CaptureTime, 120) + CHAR(13)
        FROM dbo.FACT_ServerHealth
        WHERE BlockingSessions > 0
        AND CaptureTime >= DATEADD(HOUR, -24, GETDATE())
        AND (@ServerName IS NULL OR ServerName = @ServerName);
    END
    
    -- ========================================================================
    -- BACKUP ISSUES - Using ctl.BackupCompliance
    -- Columns: ServerName, DatabaseName, Status, IsCompliant, FullAgeHours, LogAgeMinutes
    -- ========================================================================
    IF @Category IN ('Backup', 'All')
    BEGIN
        SELECT @Issues = @Issues + 
            'BACKUP ISSUE: Server ' + ServerName + 
            ' Database ' + DatabaseName +
            ' - Status: ' + ISNULL(Status, 'Unknown') +
            ', FullBackupAge: ' + CAST(ISNULL(FullAgeHours, 0) AS VARCHAR) + ' hours' + CHAR(13)
        FROM ctl.BackupCompliance
        WHERE IsCompliant = 0
        AND (@ServerName IS NULL OR ServerName = @ServerName);
    END
    
    -- ========================================================================
    -- DISK ISSUES - Using dbo.DiskSpaceInventory
    -- Columns: ServerName, DriveLetter, FreeSpacePercent, FreeSpaceGB, TotalSizeGB
    -- ========================================================================
    IF @Category IN ('Disk', 'All')
    BEGIN
        SELECT @Issues = @Issues + 
            'DISK ISSUE: Server ' + ServerName + 
            ' Drive ' + DriveLetter + ': ' + 
            CAST(CAST(FreeSpacePercent AS DECIMAL(5,2)) AS VARCHAR) + '% free' +
            ' (' + CAST(CAST(FreeSpaceGB AS DECIMAL(10,2)) AS VARCHAR) + ' GB of ' +
            CAST(CAST(TotalSizeGB AS DECIMAL(10,2)) AS VARCHAR) + ' GB)' + CHAR(13)
        FROM dbo.DiskSpaceInventory
        WHERE FreeSpacePercent < 20
        AND (@ServerName IS NULL OR ServerName = @ServerName);
    END
    
    -- ========================================================================
    -- PATCHING ISSUES - Using Patching.TaniumPatchStatus
    -- Columns: ServerName, ComplianceStatus, MissingPatches, DaysSinceLastPatch, NonComplianceReason
    -- ========================================================================
    IF @Category IN ('Patching', 'All')
    BEGIN
        SELECT @Issues = @Issues + 
            'PATCHING ISSUE: Server ' + ServerName + 
            ' - Status: ' + ISNULL(ComplianceStatus, 'Unknown') +
            ', Missing Patches: ' + CAST(ISNULL(MissingPatches, 0) AS VARCHAR) +
            ', Days Since Patch: ' + CAST(ISNULL(DaysSinceLastPatch, 0) AS VARCHAR) +
            CASE WHEN NonComplianceReason IS NOT NULL THEN ' - Reason: ' + NonComplianceReason ELSE '' END + CHAR(13)
        FROM Patching.TaniumPatchStatus
        WHERE ComplianceStatus IN ('Non-Compliant', 'Warning', 'Critical')
        AND IsActive = 1
        AND (@ServerName IS NULL OR ServerName = @ServerName);
    END
    
    -- ========================================================================
    -- SECURITY ISSUES - Check for permission drift
    -- ========================================================================
    IF @Category IN ('Security', 'All')
    BEGIN
        IF OBJECT_ID('security.PermissionDrift', 'U') IS NOT NULL
        BEGIN
            DECLARE @DriftCount INT;
            SELECT @DriftCount = COUNT(*) FROM security.PermissionDrift WHERE IsResolved = 0;
            IF @DriftCount > 0
                SET @Issues = @Issues + 'SECURITY: ' + CAST(@DriftCount AS VARCHAR) + ' unresolved permission drift entries' + CHAR(13);
        END
        
        IF OBJECT_ID('security.vw_MissingLocalAdmins', 'V') IS NOT NULL
        BEGIN
            DECLARE @MissingAdmins INT;
            SELECT @MissingAdmins = COUNT(*) FROM security.vw_MissingLocalAdmins;
            IF @MissingAdmins > 0
                SET @Issues = @Issues + 'SECURITY: ' + CAST(@MissingAdmins AS VARCHAR) + ' servers with missing local admin accounts' + CHAR(13);
        END
    END
    
    -- If no issues found
    IF @Issues = ''
        SET @Issues = 'No issues detected for the specified criteria.';
    
    -- Build AI prompt
    SET @Prompt = 'You are a SQL Server DBA AI assistant for the DBAOps monitoring system managing ' +
        '127 SQL Servers across Azure Government (CJIS compliant).
    
Analyze the following issues and provide:
1. Root cause analysis for each issue
2. Severity ranking (Critical/High/Medium/Low)
3. Specific remediation steps
4. T-SQL scripts if applicable
5. Prevention recommendations

CURRENT ISSUES:
' + @Issues + '

Provide response in this JSON format:
{
  "summary": "Brief overall assessment",
  "issueCount": <number>,
  "criticalCount": <number>,
  "issues": [
    {
      "server": "ServerName",
      "category": "Category",
      "severity": "Critical|High|Medium|Low",
      "rootCause": "Analysis",
      "remediation": "Steps to fix",
      "script": "T-SQL if applicable",
      "prevention": "How to prevent"
    }
  ],
  "prioritizedActions": ["Action 1", "Action 2"]
}';

    IF @Debug = 1
    BEGIN
        PRINT '=== ISSUES DETECTED ===';
        PRINT @Issues;
        PRINT '';
        PRINT '=== AI PROMPT ===';
        PRINT @Prompt;
    END
    
    -- Log the request
    INSERT INTO AI.DiagnosticLog (RequestType, ServerName, Category, InputContext, Prompt, Status)
    VALUES ('Diagnostic', @ServerName, @Category, @Issues, @Prompt, 'Pending');
    
    -- Return issues for manual AI submission or API call
    SELECT 
        'Copy this to Azure OpenAI or Copilot for analysis:' AS Instructions,
        @Prompt AS AIPrompt,
        @Issues AS DetectedIssues,
        DATEDIFF(MILLISECOND, @StartTime, SYSDATETIME()) AS GatherTimeMs;
    
    PRINT '';
    PRINT 'Diagnostic data gathered in ' + CAST(DATEDIFF(MILLISECOND, @StartTime, SYSDATETIME()) AS VARCHAR) + 'ms';
    PRINT 'Copy the prompt above to Azure OpenAI for analysis.';
END;

GO

-- [ai].[usp_SetAICredential]

CREATE PROCEDURE ai.usp_SetAICredential
    @Endpoint NVARCHAR(500),
    @ApiKey NVARCHAR(500),
    @DeploymentName NVARCHAR(128)
AS
BEGIN
    SET NOCOUNT ON;

    -- Update endpoint
    UPDATE ai.Configuration 
    SET ConfigValue = @Endpoint, ModifiedDate = GETDATE()
    WHERE ConfigKey = 'AzureOpenAI_Endpoint';

    -- Update API key (marked as encrypted)
    UPDATE ai.Configuration 
    SET ConfigValue = @ApiKey, IsEncrypted = 1, ModifiedDate = GETDATE()
    WHERE ConfigKey = 'AzureOpenAI_ApiKey';

    -- Update deployment name
    UPDATE ai.Configuration 
    SET ConfigValue = @DeploymentName, ModifiedDate = GETDATE()
    WHERE ConfigKey = 'AzureOpenAI_Deployment';

    -- Enable AI
    UPDATE ai.Configuration 
    SET ConfigValue = '1', ModifiedDate = GETDATE()
    WHERE ConfigKey = 'AI_Enabled';

    PRINT 'Azure OpenAI credentials configured successfully.'
    PRINT 'Endpoint: ' + @Endpoint
    PRINT 'Deployment: ' + @DeploymentName
    PRINT 'AI_Enabled: 1'
    PRINT ''
    PRINT 'Test with: EXEC ai.usp_RunDiagnostic @ServerName = ''YOURSERVER01'', @DiagnosticType = ''HealthCheck'''
END

GO

-- [alert].[usp_BackupComplianceAlert]

-- ============================================================================
-- STEP 4: Update alert.usp_BackupComplianceAlert to use ONLY Veeam data
-- ============================================================================
CREATE   PROCEDURE alert.usp_BackupComplianceAlert
AS
BEGIN
    SET NOCOUNT ON;
    
    /*
    ============================================================================
    VEEAM-ONLY BACKUP COMPLIANCE ALERT
    - All SQL Server backups are done via Veeam
    - Data source: ctl.BackupCompliance (Veeam data)
    - Threshold: Alert if no backup in last 48 hours
    ============================================================================
    */
    
    DECLARE @AlertCount INT = 0;
    DECLARE @Now DATETIME2 = SYSDATETIME();
    DECLARE @ThresholdHours INT = 48; -- CJIS requires backups within 48 hours
    
    -- Check for servers with stale Veeam backups
    SELECT @AlertCount = COUNT(DISTINCT ServerName)
    FROM ctl.BackupCompliance
    WHERE BackupSource = 'Veeam'
      AND LastBackupTime < DATEADD(HOUR, -@ThresholdHours, @Now)
      AND IsCompliant = 0;
    
    IF @AlertCount > 0
    BEGIN
        PRINT 'Found ' + CAST(@AlertCount AS VARCHAR) + ' servers with stale Veeam backups (>' + CAST(@ThresholdHours AS VARCHAR) + ' hours)';
        -- Alert is sent by usp_SendVeeamBackupAlerts job
    END
    ELSE
    BEGIN
        PRINT 'All Veeam backups are compliant';
    END
END;

GO

-- [alert].[usp_BackupComplianceDailyReport]

CREATE PROCEDURE alert.usp_BackupComplianceDailyReport
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @Subject NVARCHAR(255);
    DECLARE @TableHTML NVARCHAR(MAX);
    DECLARE @ProdSuccess INT, @ProdStale INT, @ProdCritical INT;
    DECLARE @NonProdSuccess INT, @NonProdStale INT, @NonProdCritical INT;
    DECLARE @ProdScore DECIMAL(5,2), @NonProdScore DECIMAL(5,2);
    DECLARE @OverallStatus NVARCHAR(20);
    DECLARE @Recipients NVARCHAR(500) = N'DBAOps@YOURORG.gov; SQLServerAdminNotification@YOURORG.gov';
    
    -- Get PROD metrics
    SELECT 
        @ProdSuccess = SUM(CASE WHEN c.Status = 'Success' THEN 1 ELSE 0 END),
        @ProdStale = SUM(CASE WHEN c.Status = 'Stale' THEN 1 ELSE 0 END),
        @ProdCritical = SUM(CASE WHEN c.Status = 'Critical' THEN 1 ELSE 0 END),
        @ProdScore = AVG(CAST(c.ComplianceScore AS DECIMAL(5,2)))
    FROM ctl.BackupCompliance c
    JOIN dim.[Server] s ON c.ServerName = s.ServerName
    WHERE s.Environment = 'PROD';
    
    -- Get NON-PROD metrics
    SELECT 
        @NonProdSuccess = SUM(CASE WHEN c.Status = 'Success' THEN 1 ELSE 0 END),
        @NonProdStale = SUM(CASE WHEN c.Status = 'Stale' THEN 1 ELSE 0 END),
        @NonProdCritical = SUM(CASE WHEN c.Status = 'Critical' THEN 1 ELSE 0 END),
        @NonProdScore = AVG(CAST(c.ComplianceScore AS DECIMAL(5,2)))
    FROM ctl.BackupCompliance c
    JOIN dim.[Server] s ON c.ServerName = s.ServerName
    WHERE s.Environment = 'NONPROD';
    
    SET @OverallStatus = CASE 
        WHEN ISNULL(@ProdCritical, 0) + ISNULL(@NonProdCritical, 0) > 0 THEN 'CRITICAL'
        WHEN ISNULL(@ProdStale, 0) + ISNULL(@NonProdStale, 0) > 0 THEN 'WARNING'
        ELSE 'HEALTHY'
    END;
    
    -- =====================================================
    -- SMART EMAIL LOGIC: Only send if issues exist
    -- =====================================================
    IF @OverallStatus = 'HEALTHY'
    BEGIN
        PRINT 'DBAOps | JB: All backups compliant. ' + 
            'PROD: ' + CAST(ISNULL(@ProdSuccess, 0) AS VARCHAR(10)) + ' success, ' +
            'NON-PROD: ' + CAST(ISNULL(@NonProdSuccess, 0) AS VARCHAR(10)) + ' success. ' +
            'No email sent.';
        
        -- Return summary for logging
        SELECT 'HEALTHY' AS Status,
            ISNULL(@ProdSuccess, 0) + ISNULL(@NonProdSuccess, 0) AS TotalSuccess,
            0 AS TotalStale,
            0 AS TotalCritical,
            'Email not sent - all backups compliant' AS Note;
        RETURN;
    END;
    
    -- Build HTML for non-compliant situation
    SET @TableHTML = N'<html><body style="font-family: Arial, sans-serif;">' +
        N'<h2 style="color:' + CASE @OverallStatus 
            WHEN 'CRITICAL' THEN 'red' 
            WHEN 'WARNING' THEN 'orange' 
            ELSE 'green' END + N';">DBAOps Backup Compliance Report - ' + @OverallStatus + N'</h2>' +
        N'<p>Report Generated: ' + CONVERT(NVARCHAR, GETDATE(), 120) + N'</p>' +
        N'<p><strong>This report is only sent when backup issues are detected.</strong></p>' +
        N'<h3>Compliance Summary</h3>' +
        N'<table border="1" cellpadding="8" cellspacing="0" style="border-collapse:collapse;">' +
        N'<tr style="background-color:#4472C4;color:white;">' +
        N'<th>Environment</th><th>Success</th><th>Stale</th><th>Critical</th><th>Avg Score</th></tr>' +
        N'<tr>' +
        N'<td><b>PROD</b></td>' +
        N'<td style="background-color:#90EE90;">' + CAST(ISNULL(@ProdSuccess, 0) AS NVARCHAR) + N'</td>' +
        N'<td style="background-color:' + CASE WHEN ISNULL(@ProdStale, 0) > 0 THEN '#FFD700' ELSE '#90EE90' END + N';">' + CAST(ISNULL(@ProdStale, 0) AS NVARCHAR) + N'</td>' +
        N'<td style="background-color:' + CASE WHEN ISNULL(@ProdCritical, 0) > 0 THEN '#FF6B6B' ELSE '#90EE90' END + N';">' + CAST(ISNULL(@ProdCritical, 0) AS NVARCHAR) + N'</td>' +
        N'<td>' + CAST(ISNULL(@ProdScore, 0) AS NVARCHAR) + N'%</td></tr>' +
        N'<tr>' +
        N'<td><b>NON-PROD</b></td>' +
        N'<td style="background-color:#90EE90;">' + CAST(ISNULL(@NonProdSuccess, 0) AS NVARCHAR) + N'</td>' +
        N'<td style="background-color:' + CASE WHEN ISNULL(@NonProdStale, 0) > 0 THEN '#FFD700' ELSE '#90EE90' END + N';">' + CAST(ISNULL(@NonProdStale, 0) AS NVARCHAR) + N'</td>' +
        N'<td style="background-color:' + CASE WHEN ISNULL(@NonProdCritical, 0) > 0 THEN '#FF6B6B' ELSE '#90EE90' END + N';">' + CAST(ISNULL(@NonProdCritical, 0) AS NVARCHAR) + N'</td>' +
        N'<td>' + CAST(ISNULL(@NonProdScore, 0) AS NVARCHAR) + N'%</td></tr>' +
        N'</table>';
    
    -- Add non-compliant server details
    SET @TableHTML = @TableHTML + 
        N'<h3 style="color:red;">Non-Compliant Servers</h3>' +
        N'<table border="1" cellpadding="5" cellspacing="0" style="border-collapse:collapse;">' +
        N'<tr style="background-color:#FF6B6B;color:white;">' +
        N'<th>Server</th><th>Environment</th><th>Last Backup</th><th>Hours Ago</th><th>Status</th><th>Score</th></tr>';
    
    SELECT @TableHTML = @TableHTML +
        N'<tr>' +
        N'<td>' + c.ServerName + N'</td>' +
        N'<td>' + s.Environment + N'</td>' +
        N'<td>' + ISNULL(CONVERT(NVARCHAR, c.LastBackupTime, 120), 'Never') + N'</td>' +
        N'<td>' + CAST(DATEDIFF(HOUR, c.LastBackupTime, GETDATE()) AS NVARCHAR) + N'</td>' +
        N'<td>' + c.Status + N'</td>' +
        N'<td>' + CAST(c.ComplianceScore AS NVARCHAR) + N'</td></tr>'
    FROM (
        SELECT DISTINCT ServerName, LastBackupTime, Status, ComplianceScore
        FROM ctl.BackupCompliance
        WHERE Status IN ('Stale', 'Critical')
    ) c
    JOIN dim.[Server] s ON c.ServerName = s.ServerName
    ORDER BY c.Status DESC, c.LastBackupTime;
    
    SET @TableHTML = @TableHTML + N'</table>' +
        N'<br/><p style="color:gray;">DBAOps Monitoring Framework | Author: John Bosco RHOYLLE INNOVATION EMPIRE™</p>' +
        N'<p style="color:gray;">This report only sent when issues detected. Full status in dashboard.</p>' +
        N'</body></html>';
    
    SET @Subject = CASE @OverallStatus
        WHEN 'CRITICAL' THEN N'ðŸš¨ CRITICAL: '
        WHEN 'WARNING' THEN N'âš ï¸ WARNING: '
        ELSE N'âœ… '
    END + N'DBAOps Backup Compliance Report - ' + FORMAT(GETDATE(), 'MMM dd, yyyy HH:mm');
    
    EXEC msdb.dbo.sp_send_dbmail
        @profile_name = 'DBAOpsProfile',
        @recipients = @Recipients,
        @subject = @Subject,
        @body = @TableHTML,
        @body_format = 'HTML';
    
    PRINT 'DBAOps | JB: Backup compliance report sent - ' + @OverallStatus;
    
    SELECT @OverallStatus AS Status,
        ISNULL(@ProdSuccess, 0) + ISNULL(@NonProdSuccess, 0) AS TotalSuccess,
        ISNULL(@ProdStale, 0) + ISNULL(@NonProdStale, 0) AS TotalStale,
        ISNULL(@ProdCritical, 0) + ISNULL(@NonProdCritical, 0) AS TotalCritical;
END;

GO

-- [alert].[usp_CheckVeeamBackupCompliance]

CREATE   PROCEDURE alert.usp_CheckVeeamBackupCompliance
AS
BEGIN
    SET NOCOUNT ON;
    
    SELECT 
        cms.ServerName,
        COALESCE(cms.Environment, cms.CMSEnvironment, 'Unknown') AS Environment,
        cms.ComplianceTier
    FROM config.ServerInventory cms
    WHERE cms.IsActive = 1
    AND (cms.ServerType = 'SQL Server' OR cms.ServerType IS NULL)
    ORDER BY cms.Environment, cms.ServerName;
END

GO

-- [alert].[usp_ComprehensiveComplianceReport]

CREATE PROCEDURE alert.usp_ComprehensiveComplianceReport
    @SendEmail BIT = 1,
    @AlertOnSevere BIT = 1,
    @OnlyOnIssues BIT = 1  -- NEW: Only send if issues exist
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @Subject NVARCHAR(255);
    DECLARE @TableHTML NVARCHAR(MAX);
    DECLARE @Recipients NVARCHAR(500) = N'DBAOps@YOURORG.gov; SQLServerAdminNotification@YOURORG.gov';
    DECLARE @TotalServers INT = 0;
    DECLARE @SevereCount INT = 0, @WarningCount INT = 0, @CompliantCount INT = 0;
    DECLARE @Importance VARCHAR(10) = 'Normal';
    DECLARE @ReportTime DATETIME = GETDATE();
    
    -- Collect all compliance data
    SELECT 
        h.ServerName,
        h.CaptureTime,
        ISNULL(h.CPU_Utilization, 0) AS CPU,
        CAST(ISNULL(h.Memory_UsedMB, 0) * 100.0 / NULLIF(h.Memory_TotalMB, 1) AS DECIMAL(5,1)) AS MemPct,
        ISNULL(h.Memory_PLE, 99999) AS PLE,
        ISNULL(h.BlockingSessions, 0) AS Blocking,
        ISNULL(d.DiskUsed, 0) AS DiskPct,
        ISNULL(d.CritDrive, '') AS CritDrive,
        ISNULL(d.FreeGB, 0) AS FreeGB,
        ISNULL(c.MAXDOP, 0) AS MAXDOP,
        ISNULL(c.CostThresholdForParallelism, 0) AS CostThreshold,
        ISNULL(c.MAXDOPCompliant, 1) AS MAXDOPCompliant,
        ISNULL(c.CostThresholdCompliant, 1) AS CostThresholdCompliant,
        CASE WHEN c.MaxServerMemoryMB = 2147483647 THEN 0 ELSE 1 END AS MemConfigured,
        CASE WHEN h.CPU_Utilization > 90 THEN 'SEVERE' WHEN h.CPU_Utilization > 75 THEN 'WARNING' ELSE 'OK' END AS CPUStatus,
        CASE WHEN (h.Memory_UsedMB * 100.0 / NULLIF(h.Memory_TotalMB, 1)) > 90 THEN 'SEVERE'
              WHEN (h.Memory_UsedMB * 100.0 / NULLIF(h.Memory_TotalMB, 1)) > 80 THEN 'WARNING' ELSE 'OK' END AS MemStatus,
        CASE WHEN h.Memory_PLE < 300 THEN 'SEVERE' WHEN h.Memory_PLE < 600 THEN 'WARNING' ELSE 'OK' END AS PLEStatus,
        CASE WHEN h.BlockingSessions > 5 THEN 'SEVERE' WHEN h.BlockingSessions > 0 THEN 'WARNING' ELSE 'OK' END AS BlockStatus,
        CASE WHEN ISNULL(d.DiskUsed, 0) > 85 THEN 'SEVERE' WHEN ISNULL(d.DiskUsed, 0) > 75 THEN 'WARNING' ELSE 'OK' END AS DiskStatus,
        CASE WHEN ISNULL(c.MAXDOPCompliant, 1) = 0 OR ISNULL(c.CostThresholdCompliant, 1) = 0 THEN 'WARNING' ELSE 'OK' END AS ConfigStatus
    INTO #Report
    FROM (
        SELECT *, ROW_NUMBER() OVER (PARTITION BY ServerName ORDER BY CaptureTime DESC) AS rn
        FROM metrics.FACT_ServerHealth WHERE CaptureTime >= DATEADD(HOUR, -2, GETDATE())
    ) h
    LEFT JOIN (
        SELECT ServerName, MAX(100 - FreePercent) AS DiskUsed,
               (SELECT TOP 1 DriveLetter FROM fact.DiskSpaceMetrics d2
                 WHERE d2.ServerName = d1.ServerName AND d2.CaptureDate >= DATEADD(DAY, -1, GETDATE())
                ORDER BY (100 - FreePercent) DESC) AS CritDrive,
               (SELECT TOP 1 FreeSpaceGB FROM fact.DiskSpaceMetrics d2
                 WHERE d2.ServerName = d1.ServerName AND d2.CaptureDate >= DATEADD(DAY, -1, GETDATE())
                ORDER BY (100 - FreePercent) DESC) AS FreeGB
        FROM fact.DiskSpaceMetrics d1 WHERE CaptureDate >= DATEADD(DAY, -1, GETDATE())
        GROUP BY ServerName
    ) d ON h.ServerName = d.ServerName
    LEFT JOIN (
        SELECT *, ROW_NUMBER() OVER (PARTITION BY ServerName ORDER BY CaptureTime DESC) AS crn
        FROM fact.ServerConfigCompliance WHERE CaptureTime >= DATEADD(DAY, -1, GETDATE())
    ) c ON h.ServerName = c.ServerName AND c.crn = 1
    WHERE h.rn = 1;
    
    -- Add overall status
    ALTER TABLE #Report ADD OverallStatus NVARCHAR(20), SeverityScore INT;
    
    UPDATE #Report SET 
        SeverityScore = 
            CASE WHEN CPUStatus = 'SEVERE' THEN 10 ELSE 0 END +
            CASE WHEN MemStatus = 'SEVERE' THEN 10 ELSE 0 END +
            CASE WHEN PLEStatus = 'SEVERE' THEN 8 ELSE 0 END +
            CASE WHEN BlockStatus = 'SEVERE' THEN 10 ELSE 0 END +
            CASE WHEN DiskStatus = 'SEVERE' THEN 10 ELSE 0 END +
            CASE WHEN CPUStatus = 'WARNING' THEN 3 ELSE 0 END +
            CASE WHEN MemStatus = 'WARNING' THEN 3 ELSE 0 END +
            CASE WHEN DiskStatus = 'WARNING' THEN 3 ELSE 0 END +
            CASE WHEN ConfigStatus = 'WARNING' THEN 2 ELSE 0 END;
    
    UPDATE #Report SET OverallStatus = CASE
        WHEN CPUStatus = 'SEVERE' OR MemStatus = 'SEVERE' OR PLEStatus = 'SEVERE'
              OR BlockStatus = 'SEVERE' OR DiskStatus = 'SEVERE' THEN 'SEVERE'
        WHEN CPUStatus = 'WARNING' OR MemStatus = 'WARNING' OR PLEStatus = 'WARNING'
              OR BlockStatus = 'WARNING' OR DiskStatus = 'WARNING' OR ConfigStatus = 'WARNING' THEN 'WARNING'
        ELSE 'COMPLIANT'
    END;
    
    -- Get counts
    SELECT 
        @TotalServers = COUNT(*),
        @SevereCount = SUM(CASE WHEN OverallStatus = 'SEVERE' THEN 1 ELSE 0 END),
        @WarningCount = SUM(CASE WHEN OverallStatus = 'WARNING' THEN 1 ELSE 0 END),
        @CompliantCount = SUM(CASE WHEN OverallStatus = 'COMPLIANT' THEN 1 ELSE 0 END)
    FROM #Report;
    
    -- =====================================================
    -- SMART EMAIL LOGIC: Only send if issues exist
    -- =====================================================
    IF @OnlyOnIssues = 1 AND @SevereCount = 0 AND @WarningCount = 0
    BEGIN
        PRINT 'DBAOps | JB: All ' + CAST(@TotalServers AS VARCHAR(10)) + 
            ' servers compliant. No email sent.';
        
        SELECT 
            @TotalServers AS TotalServers,
            @SevereCount AS Severe,
            @WarningCount AS Warning,
            @CompliantCount AS Compliant,
            'Email not sent - all compliant' AS Note;
        
        DROP TABLE #Report;
        RETURN;
    END;
    
    IF @SevereCount > 0 SET @Importance = 'High';
    
    -- Build HTML Report (abbreviated for space - same structure as original)
    SET @TableHTML = N'<!DOCTYPE html><html><head><style>
    body{font-family:Segoe UI,Arial;margin:20px;background:#f5f5f5}
    .container{background:white;padding:20px;border-radius:8px;box-shadow:0 2px 4px rgba(0,0,0,0.1)}
    h1{color:#2c3e50;border-bottom:3px solid #e74c3c;padding-bottom:10px}
    .box{display:inline-block;padding:15px 25px;margin:8px;border-radius:8px;color:white;font-weight:bold;font-size:16px}
    .severe{background:linear-gradient(135deg,#c0392b,#e74c3c)}
    .warning{background:linear-gradient(135deg,#d35400,#e67e22)}
    .compliant{background:linear-gradient(135deg,#27ae60,#2ecc71)}
    .total{background:#3498db}
    table{border-collapse:collapse;width:100%;margin-top:10px}
    th{background:#2c3e50;color:white;padding:10px 6px;text-align:left;font-size:11px}
    td{padding:8px 6px;border-bottom:1px solid #ecf0f1;font-size:10px}
    .row-severe{background:#fadbd8}.row-warn{background:#fdebd0}
    .s{color:#c0392b;font-weight:bold}.w{color:#d35400;font-weight:bold}.g{color:#27ae60}
    .footer{margin-top:20px;padding-top:15px;border-top:1px solid #bdc3c7;color:#7f8c8d;font-size:11px}
    </style></head><body><div class="container">
    <h1>ðŸ”’ CJIS/NIST/DISA Compliance Report</h1>
    <p>Generated: ' + CONVERT(NVARCHAR, @ReportTime, 120) + N' | Repository: ' + @@SERVERNAME + N'</p>
    <p><strong>This report is only sent when compliance issues are detected.</strong></p>
    
    <div>
    <div class="box total">ðŸ“Š Total: ' + CAST(@TotalServers AS NVARCHAR) + N'</div>
    <div class="box severe">ðŸš¨ Severe: ' + CAST(@SevereCount AS NVARCHAR) + N'</div>
    <div class="box warning">âš ï¸ Warning: ' + CAST(@WarningCount AS NVARCHAR) + N'</div>
    <div class="box compliant">âœ… Compliant: ' + CAST(@CompliantCount AS NVARCHAR) + N'</div>
    </div>';
    
    -- SEVERE SECTION
    IF @SevereCount > 0
    BEGIN
        SET @TableHTML = @TableHTML + N'
        <h2 style="color:#c0392b">ðŸš¨ SEVERE - Immediate Action Required (' + CAST(@SevereCount AS NVARCHAR) + N')</h2>
        <table><tr><th>Server</th><th>Score</th><th>CPU%</th><th>Mem%</th><th>PLE</th><th>Block</th><th>Disk%</th><th>Drive</th><th>Free GB</th></tr>';
        
        SELECT @TableHTML = @TableHTML + 
            N'<tr class="row-severe"><td><b>' + ServerName + N'</b></td>' +
            N'<td>' + CAST(SeverityScore AS NVARCHAR) + N'</td>' +
            N'<td class="' + CASE WHEN CPUStatus='SEVERE' THEN 's' WHEN CPUStatus='WARNING' THEN 'w' ELSE 'g' END + N'">' + FORMAT(CPU,'N1') + N'</td>' +
            N'<td class="' + CASE WHEN MemStatus='SEVERE' THEN 's' WHEN MemStatus='WARNING' THEN 'w' ELSE 'g' END + N'">' + FORMAT(MemPct,'N1') + N'</td>' +
            N'<td class="' + CASE WHEN PLEStatus='SEVERE' THEN 's' WHEN PLEStatus='WARNING' THEN 'w' ELSE 'g' END + N'">' + FORMAT(PLE,'N0') + N'</td>' +
            N'<td class="' + CASE WHEN BlockStatus='SEVERE' THEN 's' WHEN BlockStatus='WARNING' THEN 'w' ELSE 'g' END + N'">' + CAST(Blocking AS NVARCHAR) + N'</td>' +
            N'<td class="' + CASE WHEN DiskStatus='SEVERE' THEN 's' WHEN DiskStatus='WARNING' THEN 'w' ELSE 'g' END + N'">' + FORMAT(DiskPct,'N1') + N'</td>' +
            N'<td>' + CritDrive + N'</td>' +
            N'<td>' + FORMAT(FreeGB,'N0') + N'</td></tr>'
        FROM #Report WHERE OverallStatus = 'SEVERE' ORDER BY SeverityScore DESC;
        
        SET @TableHTML = @TableHTML + N'</table>';
    END
    
    -- WARNING SECTION
    IF @WarningCount > 0
    BEGIN
        SET @TableHTML = @TableHTML + N'
        <h2 style="color:#d35400">âš ï¸ WARNING - Attention Required (' + CAST(@WarningCount AS NVARCHAR) + N')</h2>
        <table><tr><th>Server</th><th>Score</th><th>CPU%</th><th>Mem%</th><th>PLE</th><th>Block</th><th>Disk%</th><th>Drive</th><th>Free GB</th></tr>';
        
        SELECT @TableHTML = @TableHTML + 
            N'<tr class="row-warn"><td><b>' + ServerName + N'</b></td>' +
            N'<td>' + CAST(SeverityScore AS NVARCHAR) + N'</td>' +
            N'<td class="' + CASE WHEN CPUStatus='WARNING' THEN 'w' ELSE 'g' END + N'">' + FORMAT(CPU,'N1') + N'</td>' +
            N'<td class="' + CASE WHEN MemStatus='WARNING' THEN 'w' ELSE 'g' END + N'">' + FORMAT(MemPct,'N1') + N'</td>' +
            N'<td class="' + CASE WHEN PLEStatus='WARNING' THEN 'w' ELSE 'g' END + N'">' + FORMAT(PLE,'N0') + N'</td>' +
            N'<td class="' + CASE WHEN BlockStatus='WARNING' THEN 'w' ELSE 'g' END + N'">' + CAST(Blocking AS NVARCHAR) + N'</td>' +
            N'<td class="' + CASE WHEN DiskStatus='WARNING' THEN 'w' ELSE 'g' END + N'">' + FORMAT(DiskPct,'N1') + N'</td>' +
            N'<td>' + CritDrive + N'</td>' +
            N'<td>' + FORMAT(FreeGB,'N0') + N'</td></tr>'
        FROM #Report WHERE OverallStatus = 'WARNING' ORDER BY SeverityScore DESC;
        
        SET @TableHTML = @TableHTML + N'</table>';
    END
    
    SET @TableHTML = @TableHTML + N'
    <div class="footer">
    <p><b>DBAOps Comprehensive Compliance Report</b> | Author: John Bosco RHOYLLE INNOVATION EMPIRE™</p>
    <p>This report only sent when issues detected. Full status available in dashboard.</p>
    </div></div></body></html>';
    
    -- Subject based on severity
    IF @SevereCount > 0
        SET @Subject = N'ðŸš¨ SEVERE COMPLIANCE ALERT: ' + CAST(@SevereCount AS NVARCHAR) + N' Server(s) - Immediate Action Required';
    ELSE
        SET @Subject = N'âš ï¸ Compliance Report: ' + CAST(@WarningCount AS NVARCHAR) + N' Warning Servers';
    
    -- Send email
    IF @SendEmail = 1 AND @TotalServers > 0
    BEGIN
        EXEC msdb.dbo.sp_send_dbmail
            @profile_name = 'DBAOpsProfile',
            @recipients = @Recipients,
            @subject = @Subject,
            @body = @TableHTML,
            @body_format = 'HTML',
            @importance = @Importance;
        PRINT 'DBAOps | JB: Compliance report sent - ' + CAST(@SevereCount AS VARCHAR) + ' severe, ' + CAST(@WarningCount AS VARCHAR) + ' warning';
    END
    
    SELECT 
        @TotalServers AS TotalServers,
        @SevereCount AS Severe,
        @WarningCount AS Warning,
        @CompliantCount AS Compliant,
        CONVERT(VARCHAR, @ReportTime, 120) AS ReportTime;
    
    DROP TABLE #Report;
END;

GO

-- [alert].[usp_CriticalInfrastructureReport]

CREATE PROCEDURE alert.usp_CriticalInfrastructureReport
/*
================================================================================
   DBAOps Monitoring and Compliance Framework
   Author: John Bosco RHOYLLE INNOVATION EMPIRE™, Sr SQL DBA/DBAOps Engineer
   
   UPDATED: 2026-01-27 - Only email when issues exist
================================================================================
*/
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @ServerCount INT = 0;
    DECLARE @DatabaseCount INT = 0;
    DECLARE @NonCompliantFiles INT = 0;
    DECLARE @FailedBackups INT = 0;
    DECLARE @SecurityChanges INT = 0;
    DECLARE @SysAdminCount INT = 0;
    DECLARE @CriticalIssues INT = 0;
    DECLARE @Subject NVARCHAR(255);
    DECLARE @TableHTML NVARCHAR(MAX);
    DECLARE @ProfileName NVARCHAR(128);
    DECLARE @OverallStatus NVARCHAR(20);
    DECLARE @StatusColor NVARCHAR(20);
    DECLARE @Importance NVARCHAR(10);
    
    -- Gather metrics
    SELECT @ServerCount = COUNT(DISTINCT ServerName) FROM config.ServerReachability;
    
    IF OBJECT_ID('inventory.DatabaseDetails', 'U') IS NOT NULL
        SELECT @DatabaseCount = COUNT(*) FROM inventory.DatabaseDetails 
        WHERE CollectedAt >= DATEADD(DAY, -1, GETDATE());
    
    IF OBJECT_ID('inventory.FileGrowthConfig', 'U') IS NOT NULL
        SELECT @NonCompliantFiles = COUNT(*) FROM inventory.FileGrowthConfig 
        WHERE IsCompliant = 0 AND CollectedAt >= DATEADD(DAY, -1, GETDATE());
    
    IF OBJECT_ID('ctl.BackupCompliance', 'U') IS NOT NULL
        SELECT @FailedBackups = COUNT(*) FROM ctl.BackupCompliance 
        WHERE IsCompliant = 0 AND CollectionTime >= DATEADD(DAY, -1, GETDATE());
    
    IF OBJECT_ID('audit.PermissionChanges', 'U') IS NOT NULL
        SELECT @SecurityChanges = COUNT(*) FROM audit.PermissionChanges 
        WHERE EventTime >= DATEADD(DAY, -1, GETDATE());
    
    IF OBJECT_ID('security.LoginInventory', 'U') IS NOT NULL
        SELECT @SysAdminCount = COUNT(DISTINCT LoginName) FROM security.LoginInventory 
        WHERE IsSysAdmin = 1 AND CollectedAt >= DATEADD(DAY, -1, GETDATE());
    
    -- Calculate critical issues
    SET @CriticalIssues = @NonCompliantFiles + @FailedBackups;
    
    -- Determine overall status
    SET @OverallStatus = 'HEALTHY';
    SET @StatusColor = '#44aa44';
    SET @Importance = 'Normal';
    
    IF @CriticalIssues > 10 OR @FailedBackups > 0
    BEGIN
        SET @OverallStatus = 'CRITICAL';
        SET @StatusColor = '#ff4444';
        SET @Importance = 'High';
    END
    ELSE IF @CriticalIssues > 0
    BEGIN
        SET @OverallStatus = 'WARNING';
        SET @StatusColor = '#ffaa00';
        SET @Importance = 'Normal';
    END
    
    -- =====================================================
    -- SMART EMAIL LOGIC: Only send if issues exist
    -- =====================================================
    IF @OverallStatus = 'HEALTHY'
    BEGIN
        PRINT 'DBAOps | JB: Infrastructure healthy. ' + 
            CAST(@ServerCount AS VARCHAR(10)) + ' servers, ' +
            CAST(@DatabaseCount AS VARCHAR(10)) + ' databases. No email sent.';
        
        SELECT 
            @ServerCount AS MonitoredServers,
            @DatabaseCount AS TotalDatabases,
            0 AS NonCompliantFiles,
            0 AS FailedBackups,
            @SecurityChanges AS SecurityChanges,
            @SysAdminCount AS SysAdminCount,
            'HEALTHY' AS OverallStatus,
            'Email not sent - all healthy' AS Note;
        RETURN;
    END;
    
    -- Build HTML report (only for non-healthy status)
    SET @TableHTML = N'<html><body style="font-family:Arial,sans-serif;">' +
        N'<div style="background-color:' + @StatusColor + N';color:white;padding:20px;text-align:center;">' +
        N'<h1>DBAOps Infrastructure Health Report</h1>' +
        N'<h2>Status: ' + @OverallStatus + N'</h2>' +
        N'<p>' + CONVERT(NVARCHAR, GETDATE(), 120) + N'</p>' +
        N'</div>' +
        N'<p><strong>This report is only sent when issues are detected.</strong></p>' +
        N'<h3>Issues Requiring Attention</h3>' +
        N'<table border="1" cellpadding="10" cellspacing="0" style="border-collapse:collapse;">' +
        N'<tr style="background-color:#4a90d9;color:white;">' +
        N'<th>Metric</th><th>Value</th><th>Status</th></tr>';
    
    IF @NonCompliantFiles > 0
        SET @TableHTML = @TableHTML + N'<tr><td>File Growth Non-Compliant</td><td style="color:red;font-weight:bold;">' + CAST(@NonCompliantFiles AS NVARCHAR) + N'</td><td style="color:red;">ACTION REQUIRED</td></tr>';
    
    IF @FailedBackups > 0
        SET @TableHTML = @TableHTML + N'<tr><td>Failed Backups (24h)</td><td style="color:red;font-weight:bold;">' + CAST(@FailedBackups AS NVARCHAR) + N'</td><td style="color:red;">CRITICAL</td></tr>';
    
    IF @SecurityChanges > 100
        SET @TableHTML = @TableHTML + N'<tr><td>Security Changes (24h)</td><td style="font-weight:bold;">' + CAST(@SecurityChanges AS NVARCHAR) + N'</td><td style="color:orange;">REVIEW</td></tr>';
    
    IF @SysAdminCount > 10
        SET @TableHTML = @TableHTML + N'<tr><td>SysAdmin Logins</td><td style="font-weight:bold;">' + CAST(@SysAdminCount AS NVARCHAR) + N'</td><td style="color:orange;">REVIEW</td></tr>';
    
    SET @TableHTML = @TableHTML + N'</table>' +
        N'<br/><hr/>' +
        N'<p style="color:gray;font-size:12px;">Generated by DBAOps Monitoring Framework<br/>' +
        N'Author: John Bosco RHOYLLE INNOVATION EMPIRE™ | Server: ' + @@SERVERNAME + N'</p>' +
        N'</body></html>';
    
    SET @Subject = @OverallStatus + N': DBAOps Daily Infrastructure Report';
    
    SELECT TOP 1 @ProfileName = name FROM msdb.dbo.sysmail_profile;
    
    IF @ProfileName IS NOT NULL
    BEGIN
        EXEC msdb.dbo.sp_send_dbmail
            @profile_name = @ProfileName,
            @recipients = N'DBAOps@YOURORG.gov',
            @subject = @Subject,
            @body = @TableHTML,
            @body_format = 'HTML',
            @importance = @Importance;
        
        PRINT 'DBAOps | JB: Infrastructure report sent - ' + @OverallStatus;
    END
    
    SELECT 
        @ServerCount AS MonitoredServers,
        @DatabaseCount AS TotalDatabases,
        @NonCompliantFiles AS NonCompliantFiles,
        @FailedBackups AS FailedBackups,
        @SecurityChanges AS SecurityChanges,
        @SysAdminCount AS SysAdminCount,
        @OverallStatus AS OverallStatus;
END;

GO

-- [alert].[usp_CriticalServerAlert]

CREATE   PROCEDURE alert.usp_CriticalServerAlert
    @SendEmail BIT = 1,
    @MailProfile NVARCHAR(128) = 'DBAOpsProfile',
    @Recipients NVARCHAR(MAX) = 'SQLServerAdminNotification@YOURORG.gov'
AS
BEGIN
    SET NOCOUNT ON;
    
    -- Load Default Thresholds
    DECLARE 
        @CPU_Warning DECIMAL(10,2), @CPU_Critical DECIMAL(10,2),
        @Memory_Warning DECIMAL(10,2), @Memory_Critical DECIMAL(10,2),
        @PLE_Warning DECIMAL(10,2), @PLE_Critical DECIMAL(10,2),
        @Blocking_Warning DECIMAL(10,2), @Blocking_Critical DECIMAL(10,2),
        @Disk_Warning DECIMAL(10,2), @Disk_Critical DECIMAL(10,2);
    
    SELECT @CPU_Warning = WarningThreshold, @CPU_Critical = CriticalThreshold FROM config.MonitoringThresholds WHERE MetricName = 'CPU_Percent' AND IsEnabled = 1;
    SELECT @Memory_Warning = WarningThreshold, @Memory_Critical = CriticalThreshold FROM config.MonitoringThresholds WHERE MetricName = 'Memory_Percent' AND IsEnabled = 1;
    SELECT @PLE_Warning = WarningThreshold, @PLE_Critical = CriticalThreshold FROM config.MonitoringThresholds WHERE MetricName = 'PLE_Seconds' AND IsEnabled = 1;
    SELECT @Blocking_Warning = WarningThreshold, @Blocking_Critical = CriticalThreshold FROM config.MonitoringThresholds WHERE MetricName = 'Blocking_Count' AND IsEnabled = 1;
    SELECT @Disk_Warning = WarningThreshold, @Disk_Critical = CriticalThreshold FROM config.MonitoringThresholds WHERE MetricName = 'Disk_Percent_Used' AND IsEnabled = 1;
    
    -- Set defaults
    SET @CPU_Warning = ISNULL(@CPU_Warning, 80); SET @CPU_Critical = ISNULL(@CPU_Critical, 90);
    SET @Memory_Warning = ISNULL(@Memory_Warning, 85); SET @Memory_Critical = ISNULL(@Memory_Critical, 95);
    SET @PLE_Warning = ISNULL(@PLE_Warning, 600); SET @PLE_Critical = ISNULL(@PLE_Critical, 300);
    SET @Blocking_Warning = ISNULL(@Blocking_Warning, 5); SET @Blocking_Critical = ISNULL(@Blocking_Critical, 10);
    SET @Disk_Warning = ISNULL(@Disk_Warning, 85); SET @Disk_Critical = ISNULL(@Disk_Critical, 90);
    
    -- Temp table
    DROP TABLE IF EXISTS #CriticalServers;
    CREATE TABLE #CriticalServers (
        ServerName NVARCHAR(128), Issue NVARCHAR(500),
        CPU_Percent DECIMAL(5,2), Memory_Percent DECIMAL(5,2),
        Memory_Used_GB DECIMAL(10,2), Memory_Total_GB DECIMAL(10,2),
        PLE INT, BlockingSessions INT,
        Disk_Used_Percent DECIMAL(5,2), Disk_Free_GB DECIMAL(10,2),
        DriveLetter NVARCHAR(10), Severity NVARCHAR(20), CaptureTime DATETIME
    );
    
    -- Get Disk with overrides
    DROP TABLE IF EXISTS #DiskStatus;
    SELECT 
        d.ServerName, d.DriveLetter, d.TotalSpaceGB, d.FreeSpaceGB,
        100 - d.FreePercent AS DiskUsedPercent,
        COALESCE(o.CriticalThreshold, @Disk_Critical) AS DiskCriticalThreshold,
        CASE WHEN (100 - d.FreePercent) > COALESCE(o.CriticalThreshold, @Disk_Critical) THEN 'CRITICAL' ELSE 'OK' END AS DiskStatus
    INTO #DiskStatus
    FROM (
        SELECT ServerName, DriveLetter, TotalSpaceGB, FreeSpaceGB, FreePercent,
            ROW_NUMBER() OVER (PARTITION BY ServerName, DriveLetter ORDER BY CaptureDate DESC) AS rn
        FROM fact.DiskSpaceMetrics WHERE CaptureDate >= DATEADD(DAY, -1, GETDATE())
    ) d
    LEFT JOIN config.ServerThresholdOverrides o 
        ON d.ServerName = o.ServerName AND d.DriveLetter = o.DriveLetter
        AND o.MetricName = 'Disk_Percent_Used' AND o.IsEnabled = 1
    WHERE d.rn = 1;
    
    -- Non-disk critical servers WITH MEMORY OVERRIDE SUPPORT
    INSERT INTO #CriticalServers
    SELECT h.ServerName,
        STUFF(
            CASE WHEN h.CPU_Utilization > @CPU_Critical THEN ', ðŸ”´CPU' ELSE '' END +
            CASE WHEN (h.Memory_UsedMB * 100.0 / NULLIF(h.Memory_TotalMB, 0)) > 
                 COALESCE(mo.CriticalThreshold, @Memory_Critical) THEN ', ðŸ”´MEM' ELSE '' END +
            CASE WHEN h.Memory_PLE < @PLE_Critical THEN ', ðŸ”´PLE' ELSE '' END +
            CASE WHEN h.BlockingSessions > @Blocking_Critical THEN ', ðŸ”´BLOCK' ELSE '' END,
            1, 2, ''
        ) AS Issue,
        CAST(h.CPU_Utilization AS DECIMAL(5,2)),
        CAST(h.Memory_UsedMB * 100.0 / NULLIF(h.Memory_TotalMB, 0) AS DECIMAL(5,2)),
        CAST(h.Memory_UsedMB / 1024.0 AS DECIMAL(10,2)),
        CAST(h.Memory_TotalMB / 1024.0 AS DECIMAL(10,2)),
        h.Memory_PLE, h.BlockingSessions, NULL, NULL, NULL, 'CRITICAL', h.CaptureTime
    FROM (
        SELECT *, ROW_NUMBER() OVER (PARTITION BY ServerName ORDER BY CaptureTime DESC) AS rn
        FROM metrics.FACT_ServerHealth WHERE CaptureTime >= DATEADD(HOUR, -1, GETDATE())
    ) h
    LEFT JOIN config.ServerThresholdOverrides mo 
        ON h.ServerName = mo.ServerName 
        AND mo.MetricName = 'Memory_Percent' 
        AND mo.DriveLetter IS NULL
        AND mo.IsEnabled = 1
    WHERE h.rn = 1 AND (
        h.CPU_Utilization > @CPU_Critical OR
        (h.Memory_UsedMB * 100.0 / NULLIF(h.Memory_TotalMB, 0)) > COALESCE(mo.CriticalThreshold, @Memory_Critical) OR
        h.Memory_PLE < @PLE_Critical OR
        h.BlockingSessions > @Blocking_Critical
    );
    
    -- Add critical disk issues
    INSERT INTO #CriticalServers
    SELECT ds.ServerName, 'ðŸ”´DISK (' + ds.DriveLetter + ')', NULL, NULL, NULL, NULL, NULL, NULL,
        CAST(ds.DiskUsedPercent AS DECIMAL(5,2)), ds.FreeSpaceGB, ds.DriveLetter, 'CRITICAL', GETDATE()
    FROM #DiskStatus ds WHERE ds.DiskStatus = 'CRITICAL';
    
    DECLARE @CritCount INT = (SELECT COUNT(*) FROM #CriticalServers);
    
    -- ====================================================================
    -- NEW: INSERT INTO fact_Alerts TABLE
    -- ====================================================================
    IF @CritCount > 0
    BEGIN
        INSERT INTO dbo.fact_Alerts (
            ServerName,
            AlertType,
            AlertMessage,
            Severity,
            AlertTime,
            Processed,
            AlertSeverity
        )
        SELECT 
            ServerName,
            'Performance Alert',
            Issue + ' - CPU: ' + ISNULL(CAST(CPU_Percent AS VARCHAR), 'N/A') + 
            '%, Memory: ' + ISNULL(CAST(Memory_Percent AS VARCHAR), 'N/A') + 
            '%, PLE: ' + ISNULL(CAST(PLE AS VARCHAR), 'N/A'),
            Severity,
            CaptureTime,
            0, -- Not processed yet
            'HIGH'
        FROM #CriticalServers;
        
        PRINT 'Inserted ' + CAST(@CritCount AS VARCHAR) + ' alerts into fact_Alerts';
    END
    
    -- ====================================================================
    -- NEW: SEND EMAIL ALERT
    -- ====================================================================
    IF @CritCount > 0 AND @SendEmail = 1
    BEGIN
        DECLARE @Subject NVARCHAR(255) = 'ðŸš¨ CRITICAL: ' + CAST(@CritCount AS VARCHAR) + ' SQL Servers Need Attention';
        DECLARE @Body NVARCHAR(MAX);
        
        SET @Body = '<html><head><style>
            body{font-family:Arial;font-size:14px}
            table{border-collapse:collapse;width:100%}
            th{background:#d9534f;color:white;padding:10px;text-align:left}
            td{border:1px solid #ddd;padding:8px}
            tr:nth-child(even){background:#f9f9f9}
            .critical{color:#d9534f;font-weight:bold}
        </style></head><body>';
        
        SET @Body = @Body + '<h2>ðŸš¨ Critical Server Alerts</h2>';
        SET @Body = @Body + '<p>Generated: ' + CONVERT(VARCHAR(20), GETDATE(), 120) + '</p>';
        SET @Body = @Body + '<p><strong>' + CAST(@CritCount AS VARCHAR) + ' server(s) require immediate attention!</strong></p>';
        
        SET @Body = @Body + '<table><tr><th>Server</th><th>Issue</th><th>CPU %</th><th>Memory %</th><th>PLE</th><th>Blocking</th><th>Disk %</th></tr>';
        
        SELECT @Body = @Body + 
            '<tr><td class="critical">' + ServerName + '</td>' +
            '<td>' + Issue + '</td>' +
            '<td>' + ISNULL(CAST(CPU_Percent AS VARCHAR), '-') + '</td>' +
            '<td>' + ISNULL(CAST(Memory_Percent AS VARCHAR), '-') + '</td>' +
            '<td>' + ISNULL(CAST(PLE AS VARCHAR), '-') + '</td>' +
            '<td>' + ISNULL(CAST(BlockingSessions AS VARCHAR), '-') + '</td>' +
            '<td>' + ISNULL(CAST(Disk_Used_Percent AS VARCHAR), '-') + '</td></tr>'
        FROM #CriticalServers
        ORDER BY 
            CASE WHEN Issue LIKE '%PLE%' THEN 1
                 WHEN Issue LIKE '%CPU%' THEN 2
                 WHEN Issue LIKE '%DISK%' THEN 3
                 ELSE 4 END;
        
        SET @Body = @Body + '</table>';
        SET @Body = @Body + '<p><small>DBAOps Monitoring Platform - YOURSERVER01</small></p>';
        SET @Body = @Body + '</body></html>';
        
        EXEC msdb.dbo.sp_send_dbmail
            @profile_name = @MailProfile,
            @recipients = @Recipients,
            @subject = @Subject,
            @body = @Body,
            @body_format = 'HTML';
        
        PRINT 'Alert email sent to: ' + @Recipients;
    END
    ELSE IF @CritCount = 0
    BEGIN
        PRINT 'No critical issues found. (Server overrides applied for disk and memory thresholds)';
    END
    
    -- Show results for debugging
    IF @CritCount > 0
        SELECT * FROM #CriticalServers;
    
    DROP TABLE IF EXISTS #CriticalServers, #DiskStatus;
    RETURN @CritCount;
END

GO

-- [alert].[usp_DiskSpaceSmartAlert]

CREATE   PROCEDURE alert.usp_DiskSpaceSmartAlert
    @SendEmail BIT = 1,
    @Debug BIT = 0
AS
BEGIN
    SET NOCOUNT ON;
    
    /*
    Disk Space Smart Alerting:
    - Critical (Red):   < 5% free  â†’ Alert every 5 minutes
    - Warning (Orange): < 25% free â†’ Alert every 15 minutes  
    - Caution (Yellow): < 50% free â†’ Alert every 30 minutes
    */
    
    DECLARE @Now DATETIME2 = SYSDATETIME();
    DECLARE @AlertCount INT = 0;
    DECLARE @EmailBody NVARCHAR(MAX) = '';
    DECLARE @EmailSubject NVARCHAR(255);
    DECLARE @CriticalCount INT = 0, @WarningCount INT = 0, @CautionCount INT = 0;
    
    -- Temp table for drives needing alerts
    CREATE TABLE #DrivesToAlert (
        ServerName NVARCHAR(128),
        DriveLetter CHAR(3),
        VolumeName NVARCHAR(100),
        TotalSizeGB DECIMAL(18,2),
        FreeSpaceGB DECIMAL(18,2),
        FreeSpacePercent DECIMAL(5,2),
        AlertLevel NVARCHAR(20),
        AlertColor NVARCHAR(10),
        AlertIntervalMinutes INT,
        LastAlertTime DATETIME2,
        MinutesSinceLastAlert INT
    );
    
    -- Get current disk status from dbo.DiskSpaceInventory
    -- Only get latest capture per server/drive
    ;WITH LatestDisk AS (
        SELECT 
            ServerName,
            DriveLetter,
            VolumeName,
            TotalSizeGB,
            FreeSpaceGB,
            FreeSpacePercent,
            CollectedAt,
            ROW_NUMBER() OVER (PARTITION BY ServerName, DriveLetter ORDER BY CollectedAt DESC) AS rn
        FROM dbo.DiskSpaceInventory
    )
    INSERT INTO #DrivesToAlert
    SELECT 
        d.ServerName,
        d.DriveLetter,
        d.VolumeName,
        d.TotalSizeGB,
        d.FreeSpaceGB,
        d.FreeSpacePercent,
        CASE 
            WHEN d.FreeSpacePercent < 5 THEN 'Critical'
            WHEN d.FreeSpacePercent < 25 THEN 'Warning'
            WHEN d.FreeSpacePercent < 50 THEN 'Caution'
        END AS AlertLevel,
        CASE 
            WHEN d.FreeSpacePercent < 5 THEN 'Red'
            WHEN d.FreeSpacePercent < 25 THEN 'Orange'
            WHEN d.FreeSpacePercent < 50 THEN 'Yellow'
        END AS AlertColor,
        CASE 
            WHEN d.FreeSpacePercent < 5 THEN 5
            WHEN d.FreeSpacePercent < 25 THEN 15
            WHEN d.FreeSpacePercent < 50 THEN 30
        END AS AlertIntervalMinutes,
        (SELECT MAX(AlertTime) FROM alert.DiskSpaceAlertLog l 
         WHERE l.ServerName = d.ServerName AND l.DriveLetter = d.DriveLetter) AS LastAlertTime,
        DATEDIFF(MINUTE, 
            (SELECT MAX(AlertTime) FROM alert.DiskSpaceAlertLog l 
             WHERE l.ServerName = d.ServerName AND l.DriveLetter = d.DriveLetter),
            @Now) AS MinutesSinceLastAlert
    FROM LatestDisk d
    INNER JOIN config.vw_MonitoredServers cms 
        ON d.ServerName = cms.ServerName AND cms.IsActive = 1
    WHERE d.rn = 1 
    AND d.FreeSpacePercent < 50; -- Only drives below caution threshold
    
    IF @Debug = 1
    BEGIN
        PRINT 'Drives below 50% free:';
        SELECT * FROM #DrivesToAlert ORDER BY FreeSpacePercent;
    END
    
    -- Filter to only drives that need alerting (interval has passed or first alert)
    DELETE FROM #DrivesToAlert
    WHERE LastAlertTime IS NOT NULL 
    AND MinutesSinceLastAlert < AlertIntervalMinutes;
    
    IF @Debug = 1
    BEGIN
        PRINT '';
        PRINT 'Drives needing alerts (after interval filter):';
        SELECT * FROM #DrivesToAlert ORDER BY FreeSpacePercent;
    END
    
    -- Log alerts
    INSERT INTO alert.DiskSpaceAlertLog (ServerName, DriveLetter, FreePercent, AlertLevel, AlertColor)
    SELECT ServerName, DriveLetter, FreeSpacePercent, AlertLevel, AlertColor
    FROM #DrivesToAlert;
    
    SET @AlertCount = @@ROWCOUNT;
    
    -- Count by severity
    SELECT @CriticalCount = COUNT(*) FROM #DrivesToAlert WHERE AlertLevel = 'Critical';
    SELECT @WarningCount = COUNT(*) FROM #DrivesToAlert WHERE AlertLevel = 'Warning';
    SELECT @CautionCount = COUNT(*) FROM #DrivesToAlert WHERE AlertLevel = 'Caution';
    
    -- Build email if there are alerts
    IF @AlertCount > 0 AND @SendEmail = 1
    BEGIN
        -- Set subject based on highest severity
        SET @EmailSubject = CASE 
            WHEN @CriticalCount > 0 THEN '[CRITICAL] DBAOps Disk Space Alert - ' + CAST(@CriticalCount AS VARCHAR) + ' drives critical!'
            WHEN @WarningCount > 0 THEN '[WARNING] DBAOps Disk Space Alert - ' + CAST(@WarningCount AS VARCHAR) + ' drives warning'
            ELSE '[CAUTION] DBAOps Disk Space Alert - ' + CAST(@CautionCount AS VARCHAR) + ' drives low'
        END;
        
        -- Build HTML email body
        SET @EmailBody = '
<html>
<head>
<style>
    body { font-family: Calibri, Arial, sans-serif; }
    table { border-collapse: collapse; width: 100%; }
    th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
    th { background-color: #4472C4; color: white; }
    .critical { background-color: #FF0000; color: white; font-weight: bold; }
    .warning { background-color: #FFA500; color: black; font-weight: bold; }
    .caution { background-color: #FFFF00; color: black; }
</style>
</head>
<body>
<h2>DBAOps Disk Space Alert</h2>
<p><strong>Alert Summary:</strong></p>
<ul>
    <li><span style="background-color:#FF0000;color:white;padding:2px 5px;">Critical (Red):</span> ' + CAST(@CriticalCount AS VARCHAR) + ' drives (&lt;5% free)</li>
    <li><span style="background-color:#FFA500;padding:2px 5px;">Warning (Orange):</span> ' + CAST(@WarningCount AS VARCHAR) + ' drives (&lt;25% free)</li>
    <li><span style="background-color:#FFFF00;padding:2px 5px;">Caution (Yellow):</span> ' + CAST(@CautionCount AS VARCHAR) + ' drives (&lt;50% free)</li>
</ul>
<table>
<tr>
    <th>Status</th>
    <th>Server</th>
    <th>Drive</th>
    <th>Volume</th>
    <th>Total (GB)</th>
    <th>Free (GB)</th>
    <th>Free %</th>
</tr>';

        -- Add rows
        SELECT @EmailBody = @EmailBody + 
            '<tr class="' + LOWER(AlertLevel) + '">' +
            '<td>' + AlertColor + '</td>' +
            '<td>' + ServerName + '</td>' +
            '<td>' + DriveLetter + '</td>' +
            '<td>' + ISNULL(VolumeName, '') + '</td>' +
            '<td>' + CAST(CAST(TotalSizeGB AS DECIMAL(10,2)) AS VARCHAR) + '</td>' +
            '<td>' + CAST(CAST(FreeSpaceGB AS DECIMAL(10,2)) AS VARCHAR) + '</td>' +
            '<td>' + CAST(CAST(FreeSpacePercent AS DECIMAL(5,2)) AS VARCHAR) + '%</td>' +
            '</tr>'
        FROM #DrivesToAlert
        ORDER BY FreeSpacePercent;
        
        SET @EmailBody = @EmailBody + '
</table>
<p style="margin-top: 20px; color: #666;">
<em>Alert intervals: Critical=5min, Warning=15min, Caution=30min</em><br/>
Generated by DBAOps AI Framework | ' + CONVERT(VARCHAR(30), @Now, 120) + '
</p>
</body>
</html>';

        -- Send email
        BEGIN TRY
            EXEC msdb.dbo.sp_send_dbmail
                @profile_name = 'DBAOps',
                @recipients = 'youremail@domain.gov',
                @subject = @EmailSubject,
                @body = @EmailBody,
                @body_format = 'HTML';
            
            -- Mark as sent
            UPDATE alert.DiskSpaceAlertLog
            SET EmailSent = 1, EmailSentTime = SYSDATETIME()
            WHERE AlertTime >= DATEADD(SECOND, -5, @Now);
            
            IF @Debug = 1 PRINT 'Email sent: ' + @EmailSubject;
        END TRY
        BEGIN CATCH
            IF @Debug = 1 PRINT 'Email failed: ' + ERROR_MESSAGE();
        END CATCH
    END
    
    -- Return summary
    SELECT 
        @AlertCount AS AlertsGenerated,
        @CriticalCount AS CriticalCount,
        @WarningCount AS WarningCount,
        @CautionCount AS CautionCount,
        CASE WHEN @AlertCount = 0 THEN 'No alerts needed - all drives healthy or within alert interval'
             ELSE 'Alerts logged' + CASE WHEN @SendEmail = 1 THEN ' and email sent' ELSE '' END
        END AS Status;
    
    -- Return alert details
    IF @AlertCount > 0
    BEGIN
        SELECT 
            AlertColor AS Status,
            ServerName,
            DriveLetter AS Drive,
            VolumeName,
            CAST(TotalSizeGB AS DECIMAL(10,2)) AS TotalGB,
            CAST(FreeSpaceGB AS DECIMAL(10,2)) AS FreeGB,
            CAST(FreeSpacePercent AS DECIMAL(5,2)) AS FreePercent,
            AlertLevel,
            CAST(AlertIntervalMinutes AS VARCHAR) + ' min' AS NextAlertIn
        FROM #DrivesToAlert
        ORDER BY FreeSpacePercent;
    END
    
    DROP TABLE #DrivesToAlert;
END;

GO

-- [alert].[usp_DiskSpaceThresholdAlert]

CREATE PROCEDURE alert.usp_DiskSpaceThresholdAlert
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @Subject NVARCHAR(255);
    DECLARE @TableHTML NVARCHAR(MAX);
    DECLARE @Recipients NVARCHAR(500) = N'DBAOps@YOURORG.gov; SQLServerAdminNotification@YOURORG.gov';
    DECLARE @AlertCount INT = 0;
    DECLARE @MaxSeverity NVARCHAR(20) = 'Normal';
    DECLARE @MaxSeverityColor NVARCHAR(10) = '#90EE90';
    DECLARE @Importance NVARCHAR(10);
    
    CREATE TABLE #DrivesToAlert (
        ServerName NVARCHAR(128),
        DriveLetter NVARCHAR(10),
        VolumeLabel NVARCHAR(50),
        TotalGB DECIMAL(10,2),
        FreeGB DECIMAL(10,2),
        UsedGB DECIMAL(10,2),
        UsedPercent DECIMAL(5,2),
        Severity NVARCHAR(20),
        ColorCode NVARCHAR(10),
        AlertIntervalMinutes INT,
        LastAlertTime DATETIME
    );
    
    -- Get current disk space and check against thresholds
    INSERT INTO #DrivesToAlert
    SELECT 
        d.ServerName,
        d.DriveLetter,
        d.VolumeLabel,
        d.TotalSpaceGB AS TotalGB,
        d.FreeSpaceGB AS FreeGB,
        d.TotalSpaceGB - d.FreeSpaceGB AS UsedGB,
        100 - d.FreePercent AS UsedPercent,
        t.Severity,
        t.ColorCode,
        t.AlertIntervalMinutes,
        (SELECT MAX(AlertSentAt) 
         FROM alert.DiskSpaceAlertLog l 
         WHERE l.ServerName = d.ServerName 
           AND l.DriveLetter = d.DriveLetter) AS LastAlertTime
    FROM (
        SELECT ServerName, DriveLetter, VolumeLabel, TotalSpaceGB, FreeSpaceGB, FreePercent,
               ROW_NUMBER() OVER (PARTITION BY ServerName, DriveLetter ORDER BY CaptureDate DESC) AS rn
        FROM fact.DiskSpaceMetrics
        WHERE CaptureDate >= DATEADD(DAY, -1, GETDATE())
    ) d
    JOIN config.DiskSpaceThresholds t 
        ON CAST(100 - d.FreePercent AS INT) BETWEEN t.UsedPercentMin AND t.UsedPercentMax
    WHERE d.rn = 1
      AND t.AlertIntervalMinutes > 0
      AND t.IsEnabled = 1;
    
    -- Filter to only drives that need alerting based on interval
    DELETE FROM #DrivesToAlert
    WHERE LastAlertTime IS NOT NULL 
      AND DATEDIFF(MINUTE, LastAlertTime, GETDATE()) < AlertIntervalMinutes;
    
    SELECT @AlertCount = COUNT(*) FROM #DrivesToAlert;
    
    IF @AlertCount > 0
    BEGIN
        SELECT TOP 1 @MaxSeverity = Severity, @MaxSeverityColor = ColorCode
        FROM #DrivesToAlert
        ORDER BY UsedPercent DESC;
        
        SET @TableHTML = N'<html>
<head>
<style>
    body { font-family: Arial, sans-serif; }
    table { border-collapse: collapse; width: 100%; margin-top: 10px; }
    th { background-color: #2E86AB; color: white; padding: 10px; text-align: left; }
    td { padding: 8px; border: 1px solid #ddd; }
    tr:nth-child(even) { background-color: #f9f9f9; }
    .warning { background-color: #FFD700; }
    .high { background-color: #FFA500; color: white; }
    .critical { background-color: #FF6B6B; color: white; }
    .dangerous { background-color: #8B0000; color: white; font-weight: bold; }
</style>
</head>
<body>
<h2 style="color: ' + @MaxSeverityColor + N';">ðŸ’¾ Disk Space Alert - ' + @MaxSeverity + N'</h2>
<p><strong>Report Time:</strong> ' + CONVERT(NVARCHAR, GETDATE(), 120) + N'<br/>
<strong>Drives Alerting:</strong> ' + CAST(@AlertCount AS NVARCHAR) + N'</p>

<h3>Threshold Reference</h3>
<table style="width: 60%;">
<tr><th>Severity</th><th>Usage %</th><th>Alert Interval</th></tr>
<tr class="warning"><td>Warning</td><td>50-74%</td><td>Every 60 minutes</td></tr>
<tr class="high"><td>High</td><td>75-84%</td><td>Every 30 minutes</td></tr>
<tr class="critical"><td>Critical</td><td>85-94%</td><td>Every 15 minutes</td></tr>
<tr class="dangerous"><td>DANGEROUS</td><td>95-100%</td><td>Every 5 minutes</td></tr>
</table>

<h3>Disk Space Details</h3>
<table>
<tr>
    <th>Server</th>
    <th>Drive</th>
    <th>Label</th>
    <th>Total (GB)</th>
    <th>Used (GB)</th>
    <th>Free (GB)</th>
    <th>Used %</th>
    <th>Severity</th>
</tr>';

        SELECT @TableHTML = @TableHTML + 
            N'<tr>
                <td>' + ServerName + N'</td>
                <td>' + DriveLetter + N'</td>
                <td>' + ISNULL(VolumeLabel, '') + N'</td>
                <td>' + CAST(TotalGB AS NVARCHAR) + N'</td>
                <td>' + CAST(UsedGB AS NVARCHAR) + N'</td>
                <td>' + CAST(FreeGB AS NVARCHAR) + N'</td>
                <td class="' + LOWER(Severity) + N'">' + CAST(UsedPercent AS NVARCHAR) + N'%</td>
                <td class="' + LOWER(Severity) + N'">' + Severity + N'</td>
            </tr>'
        FROM #DrivesToAlert
        ORDER BY UsedPercent DESC;

        SET @TableHTML = @TableHTML + N'</table>

<h3>Recommended Actions</h3>
<ul>';

        IF EXISTS (SELECT 1 FROM #DrivesToAlert WHERE Severity = 'DANGEROUS')
            SET @TableHTML = @TableHTML + N'<li style="color: #8B0000;"><strong>IMMEDIATE ACTION:</strong> Drives at 95%+ require immediate cleanup!</li>';
        
        IF EXISTS (SELECT 1 FROM #DrivesToAlert WHERE Severity = 'Critical')
            SET @TableHTML = @TableHTML + N'<li style="color: #FF6B6B;"><strong>Critical:</strong> Plan cleanup within 24 hours.</li>';
        
        IF EXISTS (SELECT 1 FROM #DrivesToAlert WHERE Severity = 'High')
            SET @TableHTML = @TableHTML + N'<li style="color: #FFA500;"><strong>High:</strong> Plan cleanup within 48-72 hours.</li>';
        
        IF EXISTS (SELECT 1 FROM #DrivesToAlert WHERE Severity = 'Warning')
            SET @TableHTML = @TableHTML + N'<li style="color: #FFD700;"><strong>Warning:</strong> Monitor and plan for capacity.</li>';

        SET @TableHTML = @TableHTML + N'</ul>
<br/>
<p style="color:gray;">DBAOps Monitoring Framework | Author: John Bosco RHOYLLE INNOVATION EMPIRE™</p>
</body></html>';

        IF @MaxSeverity = 'DANGEROUS'
            SET @Subject = N'ðŸš¨ DANGEROUS: Disk Space Alert - ' + CAST(@AlertCount AS NVARCHAR) + N' Drive(s)';
        ELSE IF @MaxSeverity = 'Critical'
            SET @Subject = N'âš ï¸ CRITICAL: Disk Space Alert - ' + CAST(@AlertCount AS NVARCHAR) + N' Drive(s)';
        ELSE IF @MaxSeverity = 'High'
            SET @Subject = N'âš¡ HIGH: Disk Space Alert - ' + CAST(@AlertCount AS NVARCHAR) + N' Drive(s)';
        ELSE
            SET @Subject = N'âš ï¸ WARNING: Disk Space Alert - ' + CAST(@AlertCount AS NVARCHAR) + N' Drive(s)';

        IF @MaxSeverity IN ('DANGEROUS', 'Critical')
            SET @Importance = 'High';
        ELSE
            SET @Importance = 'Normal';

        EXEC msdb.dbo.sp_send_dbmail
            @profile_name = 'DBAOpsProfile',
            @recipients = @Recipients,
            @subject = @Subject,
            @body = @TableHTML,
            @body_format = 'HTML',
            @importance = @Importance;
        
        INSERT INTO alert.DiskSpaceAlertLog (ServerName, DriveLetter, UsedPercent, Severity)
        SELECT ServerName, DriveLetter, UsedPercent, Severity
        FROM #DrivesToAlert;
        
        PRINT 'Disk space alert sent: ' + @MaxSeverity + ' - ' + CAST(@AlertCount AS VARCHAR) + ' drives';
    END
    ELSE
    BEGIN
        PRINT 'No disk space alerts needed at this time';
    END
    
    DROP TABLE #DrivesToAlert;
    
    SELECT @AlertCount AS AlertsSent, @MaxSeverity AS MaxSeverity;
END;

GO

-- [alert].[usp_FileGrowthComplianceAlert]

CREATE PROCEDURE alert.usp_FileGrowthComplianceAlert
/*
================================================================================
   DBAOps Monitoring and Compliance Framework
   Author: John Bosco RHOYLLE INNOVATION EMPIRE™, Sr SQL DBA/DBAOps Engineer
   
   Monitors file growth compliance and sends CRITICAL alert if issues found
================================================================================
*/
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @NonCompliantCount INT = 0;
    DECLARE @Subject NVARCHAR(255);
    DECLARE @Body NVARCHAR(MAX);
    DECLARE @ProfileName NVARCHAR(128);
    DECLARE @TableHTML NVARCHAR(MAX);
    
    -- Check for non-compliant files (collected in last 24 hours)
    SELECT @NonCompliantCount = COUNT(*)
    FROM inventory.FileGrowthConfig
    WHERE IsCompliant = 0
    AND CollectedAt >= DATEADD(HOUR, -24, GETDATE());
    
    IF @NonCompliantCount > 0
    BEGIN
        -- Build HTML table of non-compliant files
        SET @TableHTML = N'<html><body>' +
            N'<h2 style="color:red;">âš ï¸ CRITICAL: File Growth Compliance Alert</h2>' +
            N'<p>The following ' + CAST(@NonCompliantCount AS NVARCHAR) + ' database files are using <b>PERCENTAGE-BASED growth</b>, which can cause performance issues:</p>' +
            N'<table border="1" cellpadding="5" cellspacing="0" style="border-collapse:collapse;">' +
            N'<tr style="background-color:#ff6b6b;color:white;">' +
            N'<th>Server</th><th>Database</th><th>File Name</th><th>Type</th><th>Current Growth</th><th>Recommendation</th></tr>';
        
        SELECT @TableHTML = @TableHTML +
            N'<tr>' +
            N'<td>' + ISNULL(ServerName, 'N/A') + N'</td>' +
            N'<td>' + ISNULL(DatabaseName, 'N/A') + N'</td>' +
            N'<td>' + ISNULL(FileName, 'N/A') + N'</td>' +
            N'<td>' + ISNULL(FileType, 'N/A') + N'</td>' +
            N'<td style="color:red;font-weight:bold;">' + ISNULL(GrowthDescription, 'N/A') + N'</td>' +
            N'<td>' + CASE WHEN FileType = 'LOG' THEN '128 MB' ELSE '256 MB' END + N'</td>' +
            N'</tr>'
        FROM inventory.FileGrowthConfig
        WHERE IsCompliant = 0
        AND CollectedAt >= DATEADD(HOUR, -24, GETDATE())
        ORDER BY ServerName, DatabaseName;
        
        SET @TableHTML = @TableHTML + N'</table>' +
            N'<br/><p><b>Action Required:</b> Change file growth from percentage to fixed MB values.</p>' +
            N'<p>Generated: ' + CONVERT(NVARCHAR, GETDATE(), 120) + N'</p>' +
            N'<p style="color:gray;">DBAOps Monitoring Framework | Author: John Bosco RHOYLLE INNOVATION EMPIRE™</p>' +
            N'</body></html>';
        
        SET @Subject = N'ðŸš¨ CRITICAL: ' + CAST(@NonCompliantCount AS NVARCHAR) + ' Files Using Percentage Growth - Action Required';
        
        -- Get mail profile
        SELECT TOP 1 @ProfileName = name FROM msdb.dbo.sysmail_profile;
        
        IF @ProfileName IS NOT NULL
        BEGIN
            EXEC msdb.dbo.sp_send_dbmail
                @profile_name = @ProfileName,
                @recipients = N'DBAOps@YOURORG.gov',
                @subject = @Subject,
                @body = @TableHTML,
                @body_format = 'HTML',
                @importance = 'High';
            
            PRINT 'CRITICAL ALERT SENT: ' + CAST(@NonCompliantCount AS VARCHAR) + ' non-compliant files';
        END
    END
    ELSE
    BEGIN
        PRINT 'OK: All files are compliant';
    END
    
    -- Return status
    SELECT 
        @NonCompliantCount AS NonCompliantFiles,
        CASE WHEN @NonCompliantCount > 0 THEN 'CRITICAL' ELSE 'OK' END AS Status;
END

GO

-- [alert].[usp_ParallelismComplianceReport]

CREATE PROCEDURE alert.usp_ParallelismComplianceReport
    @SendEmail BIT = 1
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @Subject NVARCHAR(255);
    DECLARE @TableHTML NVARCHAR(MAX);
    DECLARE @Recipients NVARCHAR(500) = N'DBAOps@YOURORG.gov; SQLServerAdminNotification@YOURORG.gov';
    DECLARE @NonCompliantCount INT = 0;
    DECLARE @TotalServers INT = 0;
    DECLARE @CompliantCount INT = 0;
    DECLARE @Importance VARCHAR(10) = 'Normal';
    
    -- Get latest config for each server
    SELECT 
        c.ServerName,
        c.MAXDOP,
        c.CostThresholdForParallelism AS CostThreshold,
        c.MaxServerMemoryMB,
        c.LogicalCPUCount AS CPUs,
        c.NumaNodeCount AS NUMA,
        c.OptimizeForAdHocWorkloads AS AdHocOpt,
        c.MAXDOPCompliant,
        c.CostThresholdCompliant,
        c.MemoryCompliant,
        c.OverallCompliant,
        CASE 
            WHEN c.NumaNodeCount > 1 THEN 
                CASE WHEN (c.LogicalCPUCount / c.NumaNodeCount) > 8 THEN 8 
                     ELSE (c.LogicalCPUCount / c.NumaNodeCount) END
            ELSE 
                CASE WHEN (c.LogicalCPUCount / 2) > 8 THEN 8 
                     ELSE (c.LogicalCPUCount / 2) END
        END AS RecommendedMAXDOP,
        CASE WHEN c.MAXDOP = 0 THEN 'MAXDOP=0! ' ELSE '' END +
        CASE WHEN c.MAXDOP > 8 THEN 'MAXDOP>8! ' ELSE '' END +
        CASE WHEN c.CostThresholdForParallelism < 25 THEN 'CostThreshold=' + CAST(c.CostThresholdForParallelism AS NVARCHAR) + '! ' ELSE '' END +
        CASE WHEN c.MaxServerMemoryMB = 2147483647 THEN 'MaxMem default! ' ELSE '' END AS Issues
    INTO #Config
    FROM (
        SELECT *, ROW_NUMBER() OVER (PARTITION BY ServerName ORDER BY CaptureTime DESC) AS rn
        FROM fact.ServerConfigCompliance
        WHERE CaptureTime >= DATEADD(DAY, -1, GETDATE())
    ) c
    WHERE c.rn = 1;
    
    SELECT @TotalServers = COUNT(*), 
           @NonCompliantCount = SUM(CASE WHEN OverallCompliant = 0 THEN 1 ELSE 0 END)
    FROM #Config;
    
    SET @CompliantCount = @TotalServers - @NonCompliantCount;
    IF @NonCompliantCount > 0 SET @Importance = 'High';
    
    -- Build HTML
    SET @TableHTML = N'<!DOCTYPE html><html><head><style>
body{font-family:Segoe UI,Arial;margin:20px;background:#f5f5f5}
.container{background:white;padding:20px;border-radius:8px}
h1{color:#2c3e50;border-bottom:3px solid #8e44ad;padding-bottom:10px}
h2{color:#34495e;margin-top:25px}
.box{display:inline-block;padding:15px 25px;margin:10px;border-radius:8px;color:white;font-size:16px;font-weight:bold}
.ok{background:#27ae60}.bad{background:#c0392b}.total{background:#3498db}
table{border-collapse:collapse;width:100%;margin-top:15px}
th{background:#8e44ad;color:white;padding:10px 8px;text-align:left;font-size:11px}
td{padding:8px;border-bottom:1px solid #ecf0f1;font-size:11px}
.fail{background:#fadbd8;color:#922b21;font-weight:bold}
.pass{color:#27ae60}
.fix{background:#e8f6f3;padding:10px;border-left:4px solid #1abc9c;margin:5px 0;font-family:Consolas;font-size:11px}
.footer{margin-top:20px;padding-top:15px;border-top:1px solid #bdc3c7;color:#7f8c8d;font-size:11px}
</style></head><body><div class="container">
<h1>âš™ï¸ Parallelism Compliance Report</h1>
<p>Generated: ' + CONVERT(NVARCHAR, GETDATE(), 120) + N'</p>
<div class="box total">ðŸ“Š Total: ' + CAST(@TotalServers AS NVARCHAR) + N'</div>
<div class="box ok">âœ… Compliant: ' + CAST(@CompliantCount AS NVARCHAR) + N'</div>
<div class="box bad">âŒ Non-Compliant: ' + CAST(@NonCompliantCount AS NVARCHAR) + N'</div>

<h2>ðŸ“‹ CJIS / NIST / DISA Standards</h2>
<table style="width:90%">
<tr><th>Setting</th><th>Compliant Value</th><th>Standard</th></tr>
<tr><td>MAXDOP</td><td>1-8 based on CPUs</td><td>NIST SP 800-53 SC-5, DISA STIG</td></tr>
<tr><td>Cost Threshold</td><td>â‰¥25 (50 OLTP)</td><td>NIST SC-5, CJIS 5.10.1.1</td></tr>
<tr><td>Max Server Memory</td><td>RAM - 4GB</td><td>DISA STIG, NIST SC-6</td></tr>
</table>';

    -- Non-Compliant
    IF @NonCompliantCount > 0
    BEGIN
        SET @TableHTML = @TableHTML + N'
<h2 style="color:#c0392b">âŒ Non-Compliant (' + CAST(@NonCompliantCount AS NVARCHAR) + N')</h2>
<table><tr><th>Server</th><th>MAXDOP</th><th>Recommended</th><th>Cost Threshold</th><th>Max Memory</th><th>CPUs</th><th>Issues</th></tr>';

        SELECT @TableHTML = @TableHTML + 
            N'<tr class="fail"><td><b>' + ServerName + N'</b></td>' +
            N'<td>' + CAST(MAXDOP AS NVARCHAR) + N'</td>' +
            N'<td>' + CAST(RecommendedMAXDOP AS NVARCHAR) + N'</td>' +
            N'<td>' + CAST(CostThreshold AS NVARCHAR) + N'</td>' +
            N'<td>' + CASE WHEN MaxServerMemoryMB = 2147483647 THEN 'DEFAULT!' ELSE FORMAT(MaxServerMemoryMB/1024.0,'N1') + 'GB' END + N'</td>' +
            N'<td>' + CAST(CPUs AS NVARCHAR) + N'</td>' +
            N'<td style="color:#c0392b">' + Issues + N'</td></tr>'
        FROM #Config WHERE OverallCompliant = 0 ORDER BY ServerName;

        SET @TableHTML = @TableHTML + N'</table>
<h2>ðŸ”§ Fix Scripts</h2>
<div class="fix">EXEC sp_configure ''max degree of parallelism'', 4; RECONFIGURE;</div>
<div class="fix">EXEC sp_configure ''cost threshold for parallelism'', 50; RECONFIGURE;</div>
<div class="fix">EXEC sp_configure ''max server memory (MB)'', 28672; RECONFIGURE;</div>';
    END

    -- Compliant
    IF @CompliantCount > 0
    BEGIN
        SET @TableHTML = @TableHTML + N'
<h2 style="color:#27ae60">âœ… Compliant (' + CAST(@CompliantCount AS NVARCHAR) + N')</h2>
<table><tr><th>Server</th><th>MAXDOP</th><th>Cost Threshold</th><th>Max Memory GB</th><th>CPUs</th></tr>';

        SELECT @TableHTML = @TableHTML + 
            N'<tr><td>' + ServerName + N'</td>' +
            N'<td class="pass">' + CAST(MAXDOP AS NVARCHAR) + N'</td>' +
            N'<td class="pass">' + CAST(CostThreshold AS NVARCHAR) + N'</td>' +
            N'<td class="pass">' + FORMAT(MaxServerMemoryMB/1024.0,'N1') + N'</td>' +
            N'<td>' + CAST(CPUs AS NVARCHAR) + N'</td></tr>'
        FROM #Config WHERE OverallCompliant = 1 ORDER BY ServerName;

        SET @TableHTML = @TableHTML + N'</table>';
    END

    SET @TableHTML = @TableHTML + N'
<div class="footer"><b>DBAOps</b> | Author: John Bosco RHOYLLE INNOVATION EMPIRE™ | CJIS | NIST | DISA</div></div></body></html>';

    -- Subject
    IF @NonCompliantCount > 0
        SET @Subject = N'âš ï¸ Parallelism: ' + CAST(@NonCompliantCount AS NVARCHAR) + N' Non-Compliant Servers';
    ELSE
        SET @Subject = N'âœ… Parallelism: All ' + CAST(@TotalServers AS NVARCHAR) + N' Servers Compliant';

    IF @SendEmail = 1 AND @TotalServers > 0
    BEGIN
        EXEC msdb.dbo.sp_send_dbmail
            @profile_name = 'DBAOpsProfile',
            @recipients = @Recipients,
            @subject = @Subject,
            @body = @TableHTML,
            @body_format = 'HTML',
            @importance = @Importance;
        PRINT 'Report sent: ' + @Subject;
    END
    
    SELECT @TotalServers AS TotalServers, @CompliantCount AS Compliant, @NonCompliantCount AS NonCompliant;
    DROP TABLE #Config;
END;

GO

-- [alert].[usp_SecurityAuditAlert]

CREATE PROCEDURE alert.usp_SecurityAuditAlert
/*
================================================================================
   DBAOps Monitoring and Compliance Framework
   Author: John Bosco RHOYLLE INNOVATION EMPIRE™, Sr SQL DBA/DBAOps Engineer
   
   Monitors security changes and sends alert for suspicious activity
================================================================================
*/
    @ThresholdHours INT = 24,
    @AlertThreshold INT = 100
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @PermissionChanges INT = 0;
    DECLARE @ObjectChanges INT = 0;
    DECLARE @SysAdminCount INT = 0;
    DECLARE @TotalChanges INT = 0;
    DECLARE @Subject NVARCHAR(255);
    DECLARE @Body NVARCHAR(MAX);
    DECLARE @ProfileName NVARCHAR(128);
    DECLARE @TableHTML NVARCHAR(MAX);
    DECLARE @Severity NVARCHAR(20);
    DECLARE @HeaderColor NVARCHAR(20);
    DECLARE @HeaderText NVARCHAR(100);
    DECLARE @Importance NVARCHAR(10);
    
    -- Count permission changes
    IF OBJECT_ID('audit.PermissionChanges', 'U') IS NOT NULL
        SELECT @PermissionChanges = COUNT(*)
        FROM audit.PermissionChanges
        WHERE EventTime >= DATEADD(HOUR, -@ThresholdHours, GETDATE());
    
    -- Count object changes
    IF OBJECT_ID('audit.ObjectChanges', 'U') IS NOT NULL
        SELECT @ObjectChanges = COUNT(*)
        FROM audit.ObjectChanges
        WHERE EventTime >= DATEADD(HOUR, -@ThresholdHours, GETDATE());
    
    -- Check for sysadmin count
    IF OBJECT_ID('security.LoginInventory', 'U') IS NOT NULL
        SELECT @SysAdminCount = COUNT(DISTINCT LoginName)
        FROM security.LoginInventory
        WHERE IsSysAdmin = 1
        AND CollectedAt >= DATEADD(HOUR, -@ThresholdHours, GETDATE());
    
    SET @TotalChanges = @PermissionChanges + @ObjectChanges;
    
    -- Determine severity
    SET @Severity = 'INFO';
    SET @HeaderColor = 'blue';
    SET @HeaderText = 'Security Audit Report';
    SET @Importance = 'Normal';
    
    IF @TotalChanges > @AlertThreshold OR @SysAdminCount > 5
    BEGIN
        SET @Severity = 'CRITICAL';
        SET @HeaderColor = 'red';
        SET @HeaderText = 'CRITICAL: Security Audit Alert';
        SET @Importance = 'High';
    END
    ELSE IF @TotalChanges > 50
    BEGIN
        SET @Severity = 'WARNING';
        SET @HeaderColor = 'orange';
        SET @HeaderText = 'WARNING: Security Audit Alert';
        SET @Importance = 'Normal';
    END
    
    -- Always send report if there are changes
    IF @TotalChanges > 0 OR @SysAdminCount > 0
    BEGIN
        SET @TableHTML = N'<html><body>' +
            N'<h2 style="color:' + @HeaderColor + N';">' + @HeaderText + N'</h2>' +
            N'<p>Security changes detected in the last ' + CAST(@ThresholdHours AS NVARCHAR) + N' hours:</p>' +
            N'<table border="1" cellpadding="5" cellspacing="0" style="border-collapse:collapse;">' +
            N'<tr style="background-color:#4a90d9;color:white;">' +
            N'<th>Metric</th><th>Count</th><th>Status</th></tr>' +
            N'<tr><td>Permission Changes</td><td>' + CAST(@PermissionChanges AS NVARCHAR) + N'</td>' +
            N'<td>' + CASE WHEN @PermissionChanges > 50 THEN N'High' ELSE N'Normal' END + N'</td></tr>' +
            N'<tr><td>Object/DDL Changes</td><td>' + CAST(@ObjectChanges AS NVARCHAR) + N'</td>' +
            N'<td>' + CASE WHEN @ObjectChanges > 100 THEN N'High' ELSE N'Normal' END + N'</td></tr>' +
            N'<tr><td>SysAdmin Logins</td><td>' + CAST(@SysAdminCount AS NVARCHAR) + N'</td>' +
            N'<td>' + CASE WHEN @SysAdminCount > 10 THEN N'Review Required' ELSE N'Normal' END + N'</td></tr>' +
            N'<tr style="font-weight:bold;"><td>Total Changes</td><td>' + CAST(@TotalChanges AS NVARCHAR) + N'</td>' +
            N'<td>' + @Severity + N'</td></tr>' +
            N'</table>';
        
        SET @TableHTML = @TableHTML +
            N'<br/><p>Generated: ' + CONVERT(NVARCHAR, GETDATE(), 120) + N'</p>' +
            N'<p style="color:gray;">DBAOps Monitoring Framework | Author: John Bosco RHOYLLE INNOVATION EMPIRE™</p>' +
            N'</body></html>';
        
        SET @Subject = @HeaderText + N' - ' + CAST(@TotalChanges AS NVARCHAR) + N' Changes Detected';
        
        SELECT TOP 1 @ProfileName = name FROM msdb.dbo.sysmail_profile;
        
        IF @ProfileName IS NOT NULL
        BEGIN
            EXEC msdb.dbo.sp_send_dbmail
                @profile_name = @ProfileName,
                @recipients = N'DBAOps@YOURORG.gov',
                @subject = @Subject,
                @body = @TableHTML,
                @body_format = 'HTML',
                @importance = @Importance;
            
            PRINT 'Security Alert Sent: ' + @Severity;
        END
    END
    ELSE
    BEGIN
        PRINT 'OK: No security changes detected';
    END
    
    SELECT 
        @PermissionChanges AS PermissionChanges,
        @ObjectChanges AS ObjectChanges,
        @SysAdminCount AS SysAdminCount,
        @TotalChanges AS TotalChanges,
        @Severity AS Severity;
END

GO

-- [alert].[usp_SendVeeamBackupAlerts]

-- ============================================================================
-- STEP 2: Fix the SendVeeamBackupAlerts procedure
-- ============================================================================
CREATE   PROCEDURE alert.usp_SendVeeamBackupAlerts
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @AlertCount INT;
    DECLARE @CriticalCount INT;
    DECLARE @WarningCount INT;
    DECLARE @EmailBody NVARCHAR(MAX);
    DECLARE @EmailSubject NVARCHAR(255);
    DECLARE @Recipients NVARCHAR(500) = N'DBAOps@YOURORG.gov; SQLServerAdminNotification@YOURORG.gov; MStinchcomb@YOURORG.GOV; TVaught@YOURORG.GOV; cloudservices@YOURORG.GOV';
    DECLARE @Importance VARCHAR(10) = 'Normal';
    
    SELECT 
        @AlertCount = COUNT(*),
        @CriticalCount = SUM(CASE WHEN Status IN ('Error', 'Failed') THEN 1 ELSE 0 END),
        @WarningCount = SUM(CASE WHEN Status = 'Warning' THEN 1 ELSE 0 END)
    FROM alert.VeeamBackupAlerts
    WHERE AlertSent = 0;
    
    IF @AlertCount = 0
    BEGIN
        PRINT 'No backup alerts to send';
        RETURN;
    END
    
    IF @CriticalCount > 0
    BEGIN
        SET @EmailSubject = N'CRITICAL: ' + CAST(@CriticalCount AS NVARCHAR) + N' Veeam Backup Failure(s)';
        SET @Importance = 'High';
    END
    ELSE
        SET @EmailSubject = N'WARNING: ' + CAST(@WarningCount AS NVARCHAR) + N' Veeam Backup Warning(s)';
    
    SET @EmailBody = N'<!DOCTYPE html><html><head><style>
body{font-family:Segoe UI,Arial;background:#f5f5f5;padding:20px}
.container{max-width:900px;margin:0 auto;background:white;border-radius:8px}
.header-crit{background:#c0392b;color:white;padding:20px;border-radius:8px 8px 0 0}
.header-warn{background:#d35400;color:white;padding:20px;border-radius:8px 8px 0 0}
.content{padding:20px}
table{width:100%;border-collapse:collapse;margin:15px 0}
th{background:#2c3e50;color:white;padding:10px;text-align:left;font-size:12px}
td{padding:8px;border-bottom:1px solid #dee2e6;font-size:11px}
.crit{background:#fadbd8}
.warn{background:#fdebd0}
.thresholds{margin:20px 0;padding:15px;background:#f8f9fa;border-radius:4px}
.footer{background:#f8f9fa;padding:15px;text-align:center;font-size:11px;color:#6c757d;border-radius:0 0 8px 8px}
</style></head><body><div class="container">';

    IF @CriticalCount > 0
        SET @EmailBody = @EmailBody + N'<div class="header-crit">';
    ELSE
        SET @EmailBody = @EmailBody + N'<div class="header-warn">';

    SET @EmailBody = @EmailBody + N'<h2 style="margin:0">Veeam Backup Compliance Alert</h2>
<p style="margin:5px 0 0 0">' + CAST(@AlertCount AS NVARCHAR) + N' server(s) require attention</p></div>
<div class="content">
<div class="thresholds">
<b>Compliance Thresholds:</b><br>
PROD Servers: Full backup critical after 5 days (120 hours), TLog critical after 2 hours<br>
NON-PROD Servers: Full backup critical after 7 days (168 hours), TLog not required
</div>
<table><tr><th>Server</th><th>Status</th><th>Details</th><th>Detected</th></tr>';

    SELECT @EmailBody = @EmailBody + 
        N'<tr class="' + CASE WHEN Status IN ('Error','Failed') THEN 'crit' ELSE 'warn' END + N'">' +
        N'<td><b>' + ISNULL(ServerName, N'') + N'</b></td>' +
        N'<td>' + ISNULL(Status, N'') + N'</td>' +
        N'<td>' + ISNULL(ErrorMessage, N'') + N'</td>' +
        N'<td>' + CONVERT(NVARCHAR(20), DetectedAt, 120) + N'</td></tr>'
    FROM alert.VeeamBackupAlerts
    WHERE AlertSent = 0
    ORDER BY CASE WHEN Status IN ('Error','Failed') THEN 1 ELSE 2 END, DetectedAt DESC;

    SET @EmailBody = @EmailBody + N'</table>
<h3>Recommended Actions:</h3>
<ol>
<li>Check Veeam Backup console for job status</li>
<li>Verify SQL Server connectivity from Veeam server</li>
<li>Review backup job schedules</li>
<li>Check storage availability on backup repository</li>
</ol>
</div>
<div class="footer">DBAOps Enterprise™ Framework | YOURSERVER01 | ' + CONVERT(NVARCHAR(20), GETDATE(), 120) + N'</div>
</div></body></html>';

    EXEC msdb.dbo.sp_send_dbmail
        @profile_name = 'DBAOpsProfile',
        @recipients = @Recipients,
        @subject = @EmailSubject,
        @body = @EmailBody,
        @body_format = 'HTML',
        @importance = @Importance;
    
    UPDATE alert.VeeamBackupAlerts
    SET AlertSent = 1, AlertSentAt = GETDATE()
    WHERE AlertSent = 0;
    
    PRINT 'Sent backup alert for ' + CAST(@AlertCount AS VARCHAR) + ' server(s)';
END;

GO

-- [alert].[usp_ServerReachabilityCheck]

CREATE PROCEDURE alert.usp_ServerReachabilityCheck
/*
================================================================================
   DBAOps Monitoring and Compliance Framework
   Author: John Bosco RHOYLLE INNOVATION EMPIRE™, Sr SQL DBA/DBAOps Engineer
   
   Checks server connectivity and sends CRITICAL alert for unreachable servers
   Runs every 15 minutes
================================================================================
*/
    @AlertThreshold INT = 5,  -- Alert if more than this many servers unreachable
    @SendEmailAlways BIT = 0  -- Set to 1 to always send email, 0 for only on issues
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @ServerName NVARCHAR(128);
    DECLARE @SQL NVARCHAR(500);
    DECLARE @ReachableCount INT = 0;
    DECLARE @UnreachableCount INT = 0;
    DECLARE @TotalServers INT = 0;
    DECLARE @NewlyUnreachable INT = 0;
    DECLARE @Subject NVARCHAR(255);
    DECLARE @TableHTML NVARCHAR(MAX);
    DECLARE @ProfileName NVARCHAR(128);
    
    -- Create temp table for results
    CREATE TABLE #ServerStatus (
        ServerName NVARCHAR(128),
        IsReachable BIT,
        ErrorMessage NVARCHAR(500),
        CheckTime DATETIME DEFAULT GETDATE()
    );
    
    PRINT 'Starting server reachability check: ' + CONVERT(VARCHAR, GETDATE(), 120);
    PRINT '';
    
    -- Get all servers to check
    DECLARE server_cursor CURSOR FAST_FORWARD FOR
    SELECT DISTINCT ServerName 
    FROM config.ServerReachability
    WHERE ServerName IS NOT NULL
    ORDER BY ServerName;
    
    OPEN server_cursor;
    FETCH NEXT FROM server_cursor INTO @ServerName;
    
    WHILE @@FETCH_STATUS = 0
    BEGIN
        SET @TotalServers = @TotalServers + 1;
        
        BEGIN TRY
            -- Try to connect and run simple query
            SET @SQL = N'SELECT 1 FROM [' + @ServerName + N'].master.sys.databases WHERE database_id = 1';
            EXEC sp_executesql @SQL;
            
            -- Success
            INSERT INTO #ServerStatus (ServerName, IsReachable, ErrorMessage)
            VALUES (@ServerName, 1, NULL);
            
            SET @ReachableCount = @ReachableCount + 1;
            
            -- Update reachability table
            UPDATE config.ServerReachability
            SET Reachable = 1, CheckedOn = GETDATE()
            WHERE ServerName = @ServerName;
        END TRY
        BEGIN CATCH
            -- Failed
            INSERT INTO #ServerStatus (ServerName, IsReachable, ErrorMessage)
            VALUES (@ServerName, 0, LEFT(ERROR_MESSAGE(), 500));
            
            SET @UnreachableCount = @UnreachableCount + 1;
            
            -- Check if this is newly unreachable (was reachable before)
            IF EXISTS (SELECT 1 FROM config.ServerReachability 
                       WHERE ServerName = @ServerName AND Reachable = 1)
            BEGIN
                SET @NewlyUnreachable = @NewlyUnreachable + 1;
            END
            
            -- Update reachability table
            UPDATE config.ServerReachability
            SET Reachable = 0, CheckedOn = GETDATE()
            WHERE ServerName = @ServerName;
            
            PRINT '  [UNREACHABLE] ' + @ServerName;
        END CATCH
        
        FETCH NEXT FROM server_cursor INTO @ServerName;
    END
    
    CLOSE server_cursor;
    DEALLOCATE server_cursor;
    
    PRINT '';
    PRINT 'Results: ' + CAST(@ReachableCount AS VARCHAR) + ' reachable, ' + 
          CAST(@UnreachableCount AS VARCHAR) + ' unreachable of ' + 
          CAST(@TotalServers AS VARCHAR) + ' total';
    
    -- Send alert if threshold exceeded or newly unreachable servers
    IF @UnreachableCount > @AlertThreshold OR @NewlyUnreachable > 0 OR @SendEmailAlways = 1
    BEGIN
        SET @TableHTML = N'<html><body>' +
            N'<h2 style="color:red;">ðŸš¨ CRITICAL: Server Reachability Alert</h2>' +
            N'<p><b>' + CAST(@UnreachableCount AS NVARCHAR) + N'</b> of <b>' + 
            CAST(@TotalServers AS NVARCHAR) + N'</b> servers are unreachable!</p>';
        
        IF @NewlyUnreachable > 0
            SET @TableHTML = @TableHTML + N'<p style="color:red;font-weight:bold;">âš ï¸ ' + 
                CAST(@NewlyUnreachable AS NVARCHAR) + N' server(s) became unreachable since last check!</p>';
        
        SET @TableHTML = @TableHTML +
            N'<table border="1" cellpadding="5" cellspacing="0" style="border-collapse:collapse;">' +
            N'<tr style="background-color:#ff6b6b;color:white;">' +
            N'<th>Server Name</th><th>Status</th><th>Error</th></tr>';
        
        -- Add unreachable servers to table
        SELECT @TableHTML = @TableHTML +
            N'<tr>' +
            N'<td>' + ServerName + N'</td>' +
            N'<td style="color:red;font-weight:bold;">UNREACHABLE</td>' +
            N'<td style="font-size:11px;">' + ISNULL(LEFT(ErrorMessage, 100), 'Connection failed') + N'</td>' +
            N'</tr>'
        FROM #ServerStatus
        WHERE IsReachable = 0
        ORDER BY ServerName;
        
        SET @TableHTML = @TableHTML + N'</table>' +
            N'<br/><p><b>Summary:</b></p>' +
            N'<ul>' +
            N'<li>Total Servers: ' + CAST(@TotalServers AS NVARCHAR) + N'</li>' +
            N'<li style="color:green;">Reachable: ' + CAST(@ReachableCount AS NVARCHAR) + N'</li>' +
            N'<li style="color:red;">Unreachable: ' + CAST(@UnreachableCount AS NVARCHAR) + N'</li>' +
            N'</ul>' +
            N'<p>Check Time: ' + CONVERT(NVARCHAR, GETDATE(), 120) + N'</p>' +
            N'<p style="color:gray;">DBAOps Monitoring Framework | Author: John Bosco RHOYLLE INNOVATION EMPIRE™</p>' +
            N'</body></html>';
        
        IF @NewlyUnreachable > 0
            SET @Subject = N'ðŸš¨ CRITICAL: ' + CAST(@NewlyUnreachable AS NVARCHAR) + 
                N' Server(s) Just Went DOWN - Immediate Action Required';
        ELSE
            SET @Subject = N'âš ï¸ WARNING: ' + CAST(@UnreachableCount AS NVARCHAR) + 
                N' of ' + CAST(@TotalServers AS NVARCHAR) + N' Servers Unreachable';
        
        SELECT TOP 1 @ProfileName = name FROM msdb.dbo.sysmail_profile;
        
        IF @ProfileName IS NOT NULL
        BEGIN
            EXEC msdb.dbo.sp_send_dbmail
                @profile_name = @ProfileName,
                @recipients = N'DBAOps@YOURORG.gov',
                @subject = @Subject,
                @body = @TableHTML,
                @body_format = 'HTML',
                @importance = 'High';
            
            PRINT 'ALERT SENT: ' + CAST(@UnreachableCount AS VARCHAR) + ' unreachable servers';
        END
    END
    ELSE
    BEGIN
        PRINT 'OK: All servers within threshold';
    END
    
    -- Return summary
    SELECT 
        @TotalServers AS TotalServers,
        @ReachableCount AS ReachableServers,
        @UnreachableCount AS UnreachableServers,
        @NewlyUnreachable AS NewlyUnreachable,
        CASE WHEN @UnreachableCount > @AlertThreshold THEN 'CRITICAL' 
             WHEN @UnreachableCount > 0 THEN 'WARNING'
             ELSE 'OK' END AS Status;
    
    DROP TABLE #ServerStatus;
END

GO

-- [alert].[usp_UnifiedComplianceReport]

CREATE PROCEDURE alert.usp_UnifiedComplianceReport
    @SendEmail BIT = 1,
    @OnlyOnIssues BIT = 1  -- NEW: Only send if issues exist
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @Subject NVARCHAR(255);
    DECLARE @TableHTML NVARCHAR(MAX);
    DECLARE @Recipients NVARCHAR(500) = N'DBAOps@YOURORG.gov; SQLServerAdminNotification@YOURORG.gov';
    DECLARE @TotalServers INT = 0;
    DECLARE @HealthCritical INT = 0, @HealthWarning INT = 0, @HealthOK INT = 0;
    DECLARE @DiskCritical INT = 0, @DiskWarning INT = 0;
    DECLARE @ConfigNonCompliant INT = 0;
    DECLARE @OverallNonCompliant INT = 0;
    DECLARE @Importance VARCHAR(10) = 'Normal';
    
    -- Build compliance data
    SELECT 
        h.ServerName,
        h.CPU_Utilization AS CPU,
        CAST(h.Memory_UsedMB * 100.0 / NULLIF(h.Memory_TotalMB, 0) AS DECIMAL(5,1)) AS MemPct,
        h.Memory_PLE AS PLE,
        h.BlockingSessions AS Blocking,
        ISNULL(d.DiskUsed, 0) AS DiskPct,
        ISNULL(d.CritDrive, '') AS CritDrive,
        ISNULL(d.FreeGB, 0) AS FreeGB,
        c.MAXDOP,
        c.CostThresholdForParallelism AS CostThreshold,
        c.MAXDOPCompliant,
        c.CostThresholdCompliant,
        CASE WHEN h.CPU_Utilization > 90 THEN 'CRITICAL' WHEN h.CPU_Utilization > 75 THEN 'WARNING' ELSE 'OK' END AS CPUStatus,
        CASE WHEN (h.Memory_UsedMB * 100.0 / NULLIF(h.Memory_TotalMB, 0)) > 90 THEN 'CRITICAL'
              WHEN (h.Memory_UsedMB * 100.0 / NULLIF(h.Memory_TotalMB, 0)) > 80 THEN 'WARNING' ELSE 'OK' END AS MemStatus,
        CASE WHEN h.Memory_PLE < 300 THEN 'CRITICAL' WHEN h.Memory_PLE < 600 THEN 'WARNING' ELSE 'OK' END AS PLEStatus,
        CASE WHEN h.BlockingSessions > 5 THEN 'CRITICAL' WHEN h.BlockingSessions > 0 THEN 'WARNING' ELSE 'OK' END AS BlockStatus,
        CASE WHEN ISNULL(d.DiskUsed, 0) > 85 THEN 'CRITICAL' WHEN ISNULL(d.DiskUsed, 0) > 75 THEN 'WARNING' ELSE 'OK' END AS DiskStatus,
        CASE WHEN c.OverallCompliant = 0 THEN 'NON-COMPLIANT' ELSE 'OK' END AS ConfigStatus
    INTO #Compliance
    FROM (
        SELECT *, ROW_NUMBER() OVER (PARTITION BY ServerName ORDER BY CaptureTime DESC) AS rn
        FROM metrics.FACT_ServerHealth WHERE CaptureTime >= DATEADD(HOUR, -2, GETDATE())
    ) h
    LEFT JOIN (
        SELECT ServerName, MAX(100 - FreePercent) AS DiskUsed,
               (SELECT TOP 1 DriveLetter FROM fact.DiskSpaceMetrics d2
                 WHERE d2.ServerName = d1.ServerName ORDER BY (100 - FreePercent) DESC) AS CritDrive,
               (SELECT TOP 1 FreeSpaceGB FROM fact.DiskSpaceMetrics d2
                 WHERE d2.ServerName = d1.ServerName ORDER BY (100 - FreePercent) DESC) AS FreeGB
        FROM fact.DiskSpaceMetrics d1 WHERE CaptureDate >= DATEADD(DAY, -1, GETDATE())
        GROUP BY ServerName
    ) d ON h.ServerName = d.ServerName
    LEFT JOIN (
        SELECT *, ROW_NUMBER() OVER (PARTITION BY ServerName ORDER BY CaptureTime DESC) AS crn
        FROM fact.ServerConfigCompliance WHERE CaptureTime >= DATEADD(DAY, -1, GETDATE())
    ) c ON h.ServerName = c.ServerName AND c.crn = 1
    WHERE h.rn = 1;
    
    -- Add overall status
    ALTER TABLE #Compliance ADD OverallStatus NVARCHAR(20);
    UPDATE #Compliance SET OverallStatus = CASE
        WHEN CPUStatus = 'CRITICAL' OR MemStatus = 'CRITICAL' OR PLEStatus = 'CRITICAL'
              OR BlockStatus = 'CRITICAL' OR DiskStatus = 'CRITICAL' OR ConfigStatus = 'NON-COMPLIANT' THEN 'CRITICAL'
        WHEN CPUStatus = 'WARNING' OR MemStatus = 'WARNING' OR PLEStatus = 'WARNING'
              OR BlockStatus = 'WARNING' OR DiskStatus = 'WARNING' THEN 'WARNING'
        ELSE 'COMPLIANT'
    END;
    
    -- Get counts
    SELECT 
        @TotalServers = COUNT(*),
        @HealthCritical = SUM(CASE WHEN CPUStatus='CRITICAL' OR MemStatus='CRITICAL' OR PLEStatus='CRITICAL' OR BlockStatus='CRITICAL' THEN 1 ELSE 0 END),
        @HealthWarning = SUM(CASE WHEN OverallStatus='WARNING' THEN 1 ELSE 0 END),
        @HealthOK = SUM(CASE WHEN OverallStatus='COMPLIANT' THEN 1 ELSE 0 END),
        @DiskCritical = SUM(CASE WHEN DiskStatus='CRITICAL' THEN 1 ELSE 0 END),
        @DiskWarning = SUM(CASE WHEN DiskStatus='WARNING' THEN 1 ELSE 0 END),
        @ConfigNonCompliant = SUM(CASE WHEN ConfigStatus='NON-COMPLIANT' THEN 1 ELSE 0 END),
        @OverallNonCompliant = SUM(CASE WHEN OverallStatus IN ('CRITICAL','WARNING') THEN 1 ELSE 0 END)
    FROM #Compliance;
    
    -- =====================================================
    -- SMART EMAIL LOGIC: Only send if issues exist
    -- =====================================================
    IF @OnlyOnIssues = 1 AND @OverallNonCompliant = 0
    BEGIN
        PRINT 'DBAOps | JB: All ' + CAST(@TotalServers AS VARCHAR(10)) + 
            ' servers compliant. No email sent.';
        
        SELECT 
            @TotalServers AS TotalServers,
            0 AS HealthCritical,
            0 AS HealthWarning,
            0 AS DiskCritical,
            0 AS DiskWarning,
            @HealthOK AS FullyCompliant,
            'Email not sent - all compliant' AS Note;
        
        DROP TABLE #Compliance;
        RETURN;
    END;
    
    IF @HealthCritical > 0 OR @DiskCritical > 0 OR @ConfigNonCompliant > 0
        SET @Importance = 'High';
    
    -- Build HTML Report
    SET @TableHTML = N'<!DOCTYPE html><html><head><style>
    body{font-family:Segoe UI,Arial;margin:20px;background:#f5f5f5}
    .container{background:white;padding:20px;border-radius:8px}
    h1{color:#2c3e50;border-bottom:3px solid #e74c3c;padding-bottom:10px}
    .box{display:inline-block;padding:12px 20px;margin:8px;border-radius:8px;color:white;font-weight:bold}
    .crit{background:#c0392b}.warn{background:#d35400}.ok{background:#27ae60}.info{background:#3498db}
    table{border-collapse:collapse;width:100%;margin-top:10px}
    th{background:#2c3e50;color:white;padding:10px 6px;text-align:left;font-size:11px}
    td{padding:8px 6px;border-bottom:1px solid #ecf0f1;font-size:10px}
    .row-crit{background:#fadbd8}.row-warn{background:#fdebd0}
    .c{color:#c0392b;font-weight:bold}.w{color:#d35400;font-weight:bold}.g{color:#27ae60}
    .footer{margin-top:20px;padding-top:15px;border-top:1px solid #bdc3c7;color:#7f8c8d;font-size:11px}
    </style></head><body><div class="container">
    <h1>ðŸ”’ CJIS/NIST/DISA Unified Compliance Report</h1>
    <p>Generated: ' + CONVERT(NVARCHAR, GETDATE(), 120) + N' | Server: ' + @@SERVERNAME + N'</p>
    <p><strong>This report is only sent when compliance issues are detected.</strong></p>
    
    <h2>ðŸ“Š Executive Summary</h2>
    <div class="box info">Total: ' + CAST(@TotalServers AS NVARCHAR) + N'</div>
    <div class="box crit">Critical: ' + CAST(@HealthCritical + @DiskCritical AS NVARCHAR) + N'</div>
    <div class="box warn">Warning: ' + CAST(@HealthWarning + @DiskWarning AS NVARCHAR) + N'</div>
    <div class="box ok">Compliant: ' + CAST(@HealthOK AS NVARCHAR) + N'</div>';
    
    -- CRITICAL SECTION
    IF EXISTS (SELECT 1 FROM #Compliance WHERE OverallStatus = 'CRITICAL')
    BEGIN
        SET @TableHTML = @TableHTML + N'
        <h2 style="color:#c0392b">ðŸš¨ CRITICAL - Non-Compliant (' + CAST(@HealthCritical + @DiskCritical AS NVARCHAR) + N')</h2>
        <table><tr><th>Server</th><th>CPU%</th><th>Mem%</th><th>PLE</th><th>Block</th><th>Disk%</th><th>Drive</th><th>MAXDOP</th><th>CostThr</th></tr>';
        
        SELECT @TableHTML = @TableHTML + 
            N'<tr class="row-crit"><td><b>' + ServerName + N'</b></td>' +
            N'<td class="' + CASE WHEN CPUStatus='CRITICAL' THEN 'c' WHEN CPUStatus='WARNING' THEN 'w' ELSE 'g' END + N'">' + FORMAT(ISNULL(CPU,0),'N1') + N'</td>' +
            N'<td class="' + CASE WHEN MemStatus='CRITICAL' THEN 'c' WHEN MemStatus='WARNING' THEN 'w' ELSE 'g' END + N'">' + FORMAT(ISNULL(MemPct,0),'N1') + N'</td>' +
            N'<td class="' + CASE WHEN PLEStatus='CRITICAL' THEN 'c' WHEN PLEStatus='WARNING' THEN 'w' ELSE 'g' END + N'">' + FORMAT(ISNULL(PLE,0),'N0') + N'</td>' +
            N'<td class="' + CASE WHEN BlockStatus='CRITICAL' THEN 'c' WHEN BlockStatus='WARNING' THEN 'w' ELSE 'g' END + N'">' + CAST(ISNULL(Blocking,0) AS NVARCHAR) + N'</td>' +
            N'<td class="' + CASE WHEN DiskStatus='CRITICAL' THEN 'c' WHEN DiskStatus='WARNING' THEN 'w' ELSE 'g' END + N'">' + FORMAT(ISNULL(DiskPct,0),'N1') + N'</td>' +
            N'<td>' + ISNULL(CritDrive,'') + N' ' + FORMAT(ISNULL(FreeGB,0),'N0') + N'GB</td>' +
            N'<td class="' + CASE WHEN MAXDOPCompliant=0 THEN 'c' ELSE 'g' END + N'">' + CAST(ISNULL(MAXDOP,0) AS NVARCHAR) + N'</td>' +
            N'<td class="' + CASE WHEN CostThresholdCompliant=0 THEN 'c' ELSE 'g' END + N'">' + CAST(ISNULL(CostThreshold,0) AS NVARCHAR) + N'</td></tr>'
        FROM #Compliance WHERE OverallStatus = 'CRITICAL' ORDER BY DiskPct DESC, CPU DESC;
        
        SET @TableHTML = @TableHTML + N'</table>';
    END
    
    -- WARNING SECTION
    IF EXISTS (SELECT 1 FROM #Compliance WHERE OverallStatus = 'WARNING')
    BEGIN
        SET @TableHTML = @TableHTML + N'
        <h2 style="color:#d35400">âš ï¸ WARNING (' + CAST(@HealthWarning + @DiskWarning AS NVARCHAR) + N')</h2>
        <table><tr><th>Server</th><th>CPU%</th><th>Mem%</th><th>PLE</th><th>Block</th><th>Disk%</th><th>Drive</th><th>MAXDOP</th><th>CostThr</th></tr>';
        
        SELECT @TableHTML = @TableHTML + 
            N'<tr class="row-warn"><td><b>' + ServerName + N'</b></td>' +
            N'<td class="' + CASE WHEN CPUStatus='WARNING' THEN 'w' ELSE 'g' END + N'">' + FORMAT(ISNULL(CPU,0),'N1') + N'</td>' +
            N'<td class="' + CASE WHEN MemStatus='WARNING' THEN 'w' ELSE 'g' END + N'">' + FORMAT(ISNULL(MemPct,0),'N1') + N'</td>' +
            N'<td class="' + CASE WHEN PLEStatus='WARNING' THEN 'w' ELSE 'g' END + N'">' + FORMAT(ISNULL(PLE,0),'N0') + N'</td>' +
            N'<td class="' + CASE WHEN BlockStatus='WARNING' THEN 'w' ELSE 'g' END + N'">' + CAST(ISNULL(Blocking,0) AS NVARCHAR) + N'</td>' +
            N'<td class="' + CASE WHEN DiskStatus='WARNING' THEN 'w' ELSE 'g' END + N'">' + FORMAT(ISNULL(DiskPct,0),'N1') + N'</td>' +
            N'<td>' + ISNULL(CritDrive,'') + N' ' + FORMAT(ISNULL(FreeGB,0),'N0') + N'GB</td>' +
            N'<td class="g">' + CAST(ISNULL(MAXDOP,0) AS NVARCHAR) + N'</td>' +
            N'<td class="g">' + CAST(ISNULL(CostThreshold,0) AS NVARCHAR) + N'</td></tr>'
        FROM #Compliance WHERE OverallStatus = 'WARNING' ORDER BY DiskPct DESC, CPU DESC;
        
        SET @TableHTML = @TableHTML + N'</table>';
    END
    
    SET @TableHTML = @TableHTML + N'
    <div class="footer">
    <p><b>DBAOps Unified Compliance Report</b> | Author: John Bosco RHOYLLE INNOVATION EMPIRE™</p>
    <p>This report only sent when issues detected. Full status in dashboard.</p>
    </div></div></body></html>';
    
    -- Subject
    IF @HealthCritical > 0 OR @DiskCritical > 0
        SET @Subject = N'ðŸš¨ COMPLIANCE ALERT: ' + CAST(@HealthCritical + @DiskCritical AS NVARCHAR) + N' Critical, ' + CAST(@HealthWarning + @DiskWarning AS NVARCHAR) + N' Warning';
    ELSE
        SET @Subject = N'âš ï¸ Compliance Report: ' + CAST(@HealthWarning + @DiskWarning AS NVARCHAR) + N' Warning Servers';
    
    IF @SendEmail = 1 AND @TotalServers > 0
    BEGIN
        EXEC msdb.dbo.sp_send_dbmail
            @profile_name = 'DBAOpsProfile',
            @recipients = @Recipients,
            @subject = @Subject,
            @body = @TableHTML,
            @body_format = 'HTML',
            @importance = @Importance;
        PRINT 'DBAOps | JB: Unified compliance report sent';
    END
    
    SELECT 
        @TotalServers AS TotalServers,
        @HealthCritical AS HealthCritical,
        @HealthWarning AS HealthWarning,
        @DiskCritical AS DiskCritical,
        @DiskWarning AS DiskWarning,
        @ConfigNonCompliant AS ConfigNonCompliant,
        @HealthOK AS FullyCompliant;
    
    DROP TABLE #Compliance;
END;

GO

-- [alert].[usp_UniversalHealthReport]

CREATE PROCEDURE alert.usp_UniversalHealthReport
    @ReportType VARCHAR(20) = 'HOURLY',
    @SendEmail BIT = 1
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @Subject NVARCHAR(255);
    DECLARE @TableHTML NVARCHAR(MAX);
    DECLARE @Recipients NVARCHAR(500) = N'DBAOps@YOURORG.gov; SQLServerAdminNotification@YOURORG.gov';
    DECLARE @CriticalCount INT = 0, @WarningCount INT = 0, @HealthyCount INT = 0, @TotalServers INT = 0;
    DECLARE @Importance VARCHAR(10) = 'Normal';
    
    -- Collect all health data into temp table
    SELECT 
        h.ServerName,
        h.CaptureTime,
        ISNULL(h.CPU_Utilization, 0) AS CPU_Percent,
        CASE WHEN ISNULL(h.Memory_TotalMB, 0) > 0 
             THEN CAST((ISNULL(h.Memory_UsedMB, 0) / h.Memory_TotalMB) * 100 AS DECIMAL(5,2))
             ELSE 0 END AS Memory_Percent,
        ISNULL(h.Memory_PLE, 99999) AS PLE,
        ISNULL(h.BlockingSessions, 0) AS BlockingSessions,
        ISNULL(h.ActiveSessions, 0) AS ActiveSessions,
        ISNULL(d.MaxDiskUsed, 0) AS DiskUsedPercent,
        ISNULL(b.HoursSinceBackup, 0) AS BackupAgeHours
    INTO #Health
    FROM (
        SELECT *, ROW_NUMBER() OVER (PARTITION BY ServerName ORDER BY CaptureTime DESC) AS rn
        FROM metrics.FACT_ServerHealth
        WHERE CaptureTime >= DATEADD(HOUR, -1, GETDATE())
    ) h
    LEFT JOIN (
        SELECT ServerName, MAX(100 - FreePercent) AS MaxDiskUsed
        FROM fact.DiskSpaceMetrics
        WHERE CaptureDate >= DATEADD(DAY, -1, GETDATE())
        GROUP BY ServerName
    ) d ON h.ServerName = d.ServerName
    LEFT JOIN (
        SELECT ServerName, MIN(DATEDIFF(HOUR, LastBackupTime, GETDATE())) AS HoursSinceBackup
        FROM ctl.BackupCompliance
        WHERE LastBackupTime IS NOT NULL
        GROUP BY ServerName
    ) b ON h.ServerName = b.ServerName
    WHERE h.rn = 1;
    
    -- Add status columns
    ALTER TABLE #Health ADD 
        CPUStatus NVARCHAR(20), MemoryStatus NVARCHAR(20), PLEStatus NVARCHAR(20),
        BlockingStatus NVARCHAR(20), DiskStatus NVARCHAR(20), BackupStatus NVARCHAR(20),
        OverallStatus NVARCHAR(20), Recommendations NVARCHAR(500);
    
    -- Calculate statuses per CJIS/NIST/DISA
    UPDATE #Health SET 
        CPUStatus = CASE WHEN CPU_Percent > 90 THEN 'CRITICAL' WHEN CPU_Percent > 75 THEN 'WARNING' ELSE 'HEALTHY' END,
        MemoryStatus = CASE WHEN Memory_Percent > 90 THEN 'CRITICAL' WHEN Memory_Percent > 80 THEN 'WARNING' ELSE 'HEALTHY' END,
        PLEStatus = CASE WHEN PLE < 300 THEN 'CRITICAL' WHEN PLE < 600 THEN 'WARNING' ELSE 'HEALTHY' END,
        BlockingStatus = CASE WHEN BlockingSessions > 5 THEN 'CRITICAL' WHEN BlockingSessions > 0 THEN 'WARNING' ELSE 'HEALTHY' END,
        DiskStatus = CASE WHEN DiskUsedPercent > 85 THEN 'CRITICAL' WHEN DiskUsedPercent > 75 THEN 'WARNING' ELSE 'HEALTHY' END,
        BackupStatus = CASE WHEN BackupAgeHours > 48 THEN 'CRITICAL' WHEN BackupAgeHours > 24 THEN 'WARNING' ELSE 'HEALTHY' END;
    
    UPDATE #Health SET OverallStatus = CASE 
        WHEN CPUStatus = 'CRITICAL' OR MemoryStatus = 'CRITICAL' OR PLEStatus = 'CRITICAL' 
             OR BlockingStatus = 'CRITICAL' OR DiskStatus = 'CRITICAL' OR BackupStatus = 'CRITICAL' THEN 'CRITICAL'
        WHEN CPUStatus = 'WARNING' OR MemoryStatus = 'WARNING' OR PLEStatus = 'WARNING' 
             OR BlockingStatus = 'WARNING' OR DiskStatus = 'WARNING' OR BackupStatus = 'WARNING' THEN 'WARNING'
        ELSE 'HEALTHY' END;
    
    UPDATE #Health SET Recommendations = 
        CASE WHEN CPUStatus IN ('CRITICAL','WARNING') THEN 'CPU: Investigate high-CPU queries; ' ELSE '' END +
        CASE WHEN MemoryStatus IN ('CRITICAL','WARNING') THEN 'Memory: Check memory pressure; ' ELSE '' END +
        CASE WHEN PLEStatus IN ('CRITICAL','WARNING') THEN 'PLE: Add RAM or optimize queries; ' ELSE '' END +
        CASE WHEN BlockingStatus IN ('CRITICAL','WARNING') THEN 'Blocking: Resolve blocking chains; ' ELSE '' END +
        CASE WHEN DiskStatus IN ('CRITICAL','WARNING') THEN 'Disk: Cleanup or expand storage; ' ELSE '' END +
        CASE WHEN BackupStatus IN ('CRITICAL','WARNING') THEN 'Backup: Verify backup jobs; ' ELSE '' END;
    
    SELECT 
        @TotalServers = COUNT(*),
        @CriticalCount = SUM(CASE WHEN OverallStatus = 'CRITICAL' THEN 1 ELSE 0 END),
        @WarningCount = SUM(CASE WHEN OverallStatus = 'WARNING' THEN 1 ELSE 0 END),
        @HealthyCount = SUM(CASE WHEN OverallStatus = 'HEALTHY' THEN 1 ELSE 0 END)
    FROM #Health;
    
    IF @ReportType = 'CRITICAL' AND ISNULL(@CriticalCount, 0) = 0
    BEGIN
        DROP TABLE #Health;
        SELECT 0 AS TotalServers, 0 AS Critical, 0 AS Warning, 0 AS Healthy, 'CRITICAL' AS ReportType;
        RETURN;
    END
    
    -- Build HTML
    SET @TableHTML = N'<!DOCTYPE html><html><head><style>
body{font-family:Segoe UI,Arial;margin:20px;background:#f5f5f5}
.container{background:white;padding:20px;border-radius:8px;box-shadow:0 2px 4px rgba(0,0,0,0.1)}
h1{color:#2c3e50;border-bottom:3px solid #3498db;padding-bottom:10px}
h2{color:#34495e;margin-top:25px}
.box{display:inline-block;padding:15px 25px;margin:10px;border-radius:8px;color:white;font-size:18px;font-weight:bold}
.crit{background:linear-gradient(135deg,#c0392b,#e74c3c)}
.warn{background:linear-gradient(135deg,#d35400,#e67e22)}
.ok{background:linear-gradient(135deg,#27ae60,#2ecc71)}
.total{background:#3498db}
table{border-collapse:collapse;width:100%;margin-top:15px}
th{background:linear-gradient(135deg,#2c3e50,#34495e);color:white;padding:12px 8px;text-align:left;font-size:12px}
td{padding:10px 8px;border-bottom:1px solid #ecf0f1;font-size:11px}
tr:hover{background:#f8f9fa}
.row-crit{background:#fadbd8}
.row-warn{background:#fdebd0}
.m-crit{color:#c0392b;font-weight:bold}
.m-warn{color:#d35400;font-weight:bold}
.m-ok{color:#27ae60}
.comp{width:70%;margin:20px 0}
.comp th{background:#8e44ad}
.ts{color:#7f8c8d;font-size:12px}
.rec{font-size:10px;color:#c0392b;max-width:300px}
.footer{margin-top:30px;padding-top:15px;border-top:1px solid #bdc3c7;color:#7f8c8d;font-size:11px}
</style></head><body><div class="container">
<h1>ðŸ¥ DBAOps Universal Health Report</h1>
<p class="ts">Generated: ' + CONVERT(NVARCHAR, GETDATE(), 120) + N' | Type: ' + @ReportType + N' | Server: ' + @@SERVERNAME + N'</p>
<h2>ðŸ“Š Executive Summary</h2>
<div class="box crit">ðŸš¨ Critical: ' + CAST(@CriticalCount AS NVARCHAR) + N'</div>
<div class="box warn">âš ï¸ Warning: ' + CAST(@WarningCount AS NVARCHAR) + N'</div>
<div class="box ok">âœ… Healthy: ' + CAST(@HealthyCount AS NVARCHAR) + N'</div>
<div class="box total">ðŸ“ˆ Total: ' + CAST(@TotalServers AS NVARCHAR) + N'</div>

<h2>ðŸ“‹ CJIS / NIST / DISA Compliance Thresholds</h2>
<table class="comp">
<tr><th>Metric</th><th>Warning</th><th>Critical</th><th>Standard</th></tr>
<tr><td>CPU Utilization</td><td>&gt; 75%</td><td>&gt; 90%</td><td>NIST SP 800-53</td></tr>
<tr><td>Memory Utilization</td><td>&gt; 80%</td><td>&gt; 90%</td><td>DISA STIG</td></tr>
<tr><td>Page Life Expectancy</td><td>&lt; 600</td><td>&lt; 300</td><td>Best Practice</td></tr>
<tr><td>Blocking Sessions</td><td>&gt; 0</td><td>&gt; 5</td><td>CJIS Security Policy</td></tr>
<tr><td>Disk Usage</td><td>&gt; 75%</td><td>&gt; 85%</td><td>NIST SP 800-53</td></tr>
<tr><td>Backup Age</td><td>&gt; 24h</td><td>&gt; 48h</td><td>CJIS 5.4.1.1</td></tr>
</table>';

    -- Critical Section
    IF @CriticalCount > 0
    BEGIN
        SET @TableHTML = @TableHTML + N'<h2 style="color:#c0392b">ðŸš¨ CRITICAL - Immediate Action Required (' + CAST(@CriticalCount AS NVARCHAR) + N')</h2>
<table><tr><th>Server</th><th>CPU%</th><th>Mem%</th><th>PLE</th><th>Block</th><th>Disk%</th><th>Backup(h)</th><th>Recommended Action</th></tr>';
        
        SELECT @TableHTML = @TableHTML + N'<tr class="row-crit">' +
            N'<td><b>' + ServerName + N'</b></td>' +
            N'<td class="' + CASE WHEN CPUStatus='CRITICAL' THEN 'm-crit' WHEN CPUStatus='WARNING' THEN 'm-warn' ELSE 'm-ok' END + N'">' + FORMAT(CPU_Percent,'N1') + N'</td>' +
            N'<td class="' + CASE WHEN MemoryStatus='CRITICAL' THEN 'm-crit' WHEN MemoryStatus='WARNING' THEN 'm-warn' ELSE 'm-ok' END + N'">' + FORMAT(Memory_Percent,'N1') + N'</td>' +
            N'<td class="' + CASE WHEN PLEStatus='CRITICAL' THEN 'm-crit' WHEN PLEStatus='WARNING' THEN 'm-warn' ELSE 'm-ok' END + N'">' + FORMAT(PLE,'N0') + N'</td>' +
            N'<td class="' + CASE WHEN BlockingStatus='CRITICAL' THEN 'm-crit' WHEN BlockingStatus='WARNING' THEN 'm-warn' ELSE 'm-ok' END + N'">' + CAST(BlockingSessions AS NVARCHAR) + N'</td>' +
            N'<td class="' + CASE WHEN DiskStatus='CRITICAL' THEN 'm-crit' WHEN DiskStatus='WARNING' THEN 'm-warn' ELSE 'm-ok' END + N'">' + FORMAT(DiskUsedPercent,'N1') + N'</td>' +
            N'<td class="' + CASE WHEN BackupStatus='CRITICAL' THEN 'm-crit' WHEN BackupStatus='WARNING' THEN 'm-warn' ELSE 'm-ok' END + N'">' + CAST(BackupAgeHours AS NVARCHAR) + N'</td>' +
            N'<td class="rec">' + ISNULL(Recommendations,'') + N'</td></tr>'
        FROM #Health WHERE OverallStatus = 'CRITICAL' ORDER BY DiskUsedPercent DESC, CPU_Percent DESC;
        
        SET @TableHTML = @TableHTML + N'</table>';
    END
    
    -- Warning Section
    IF @WarningCount > 0 AND @ReportType = 'HOURLY'
    BEGIN
        SET @TableHTML = @TableHTML + N'<h2 style="color:#d35400">âš ï¸ WARNING - Attention Required (' + CAST(@WarningCount AS NVARCHAR) + N')</h2>
<table><tr><th>Server</th><th>CPU%</th><th>Mem%</th><th>PLE</th><th>Block</th><th>Disk%</th><th>Backup(h)</th><th>Recommended Action</th></tr>';
        
        SELECT @TableHTML = @TableHTML + N'<tr class="row-warn">' +
            N'<td><b>' + ServerName + N'</b></td>' +
            N'<td class="' + CASE WHEN CPUStatus='CRITICAL' THEN 'm-crit' WHEN CPUStatus='WARNING' THEN 'm-warn' ELSE 'm-ok' END + N'">' + FORMAT(CPU_Percent,'N1') + N'</td>' +
            N'<td class="' + CASE WHEN MemoryStatus='CRITICAL' THEN 'm-crit' WHEN MemoryStatus='WARNING' THEN 'm-warn' ELSE 'm-ok' END + N'">' + FORMAT(Memory_Percent,'N1') + N'</td>' +
            N'<td class="' + CASE WHEN PLEStatus='CRITICAL' THEN 'm-crit' WHEN PLEStatus='WARNING' THEN 'm-warn' ELSE 'm-ok' END + N'">' + FORMAT(PLE,'N0') + N'</td>' +
            N'<td class="' + CASE WHEN BlockingStatus='CRITICAL' THEN 'm-crit' WHEN BlockingStatus='WARNING' THEN 'm-warn' ELSE 'm-ok' END + N'">' + CAST(BlockingSessions AS NVARCHAR) + N'</td>' +
            N'<td class="' + CASE WHEN DiskStatus='CRITICAL' THEN 'm-crit' WHEN DiskStatus='WARNING' THEN 'm-warn' ELSE 'm-ok' END + N'">' + FORMAT(DiskUsedPercent,'N1') + N'</td>' +
            N'<td class="' + CASE WHEN BackupStatus='CRITICAL' THEN 'm-crit' WHEN BackupStatus='WARNING' THEN 'm-warn' ELSE 'm-ok' END + N'">' + CAST(BackupAgeHours AS NVARCHAR) + N'</td>' +
            N'<td class="rec">' + ISNULL(Recommendations,'') + N'</td></tr>'
        FROM #Health WHERE OverallStatus = 'WARNING' ORDER BY DiskUsedPercent DESC, CPU_Percent DESC;
        
        SET @TableHTML = @TableHTML + N'</table>';
    END
    
    -- Healthy Summary
    IF @ReportType = 'HOURLY' AND @HealthyCount > 0
        SET @TableHTML = @TableHTML + N'<h2 style="color:#27ae60">âœ… Healthy Servers (' + CAST(@HealthyCount AS NVARCHAR) + N')</h2>
<p>All metrics within CJIS/NIST/DISA compliance thresholds.</p>';
    
    SET @TableHTML = @TableHTML + N'<div class="footer">
<p><b>DBAOps Monitoring Framework</b> | Author: John Bosco RHOYLLE INNOVATION EMPIRE™</p>
<p>Standards: CJIS Security Policy 5.4 | NIST SP 800-53 | DISA STIG</p>
<p>Schedule: Critical alerts every 5 min | Hourly summary report</p>
</div></div></body></html>';

    -- Subject line
    IF @ReportType = 'CRITICAL'
        SET @Subject = N'ðŸš¨ CRITICAL: ' + CAST(@CriticalCount AS NVARCHAR) + N' Server(s) Need Immediate Action';
    ELSE
        SET @Subject = N'ðŸ“Š Hourly Health: ' + 
            CASE WHEN @CriticalCount > 0 THEN CAST(@CriticalCount AS NVARCHAR) + N' Crit, ' ELSE '' END +
            CASE WHEN @WarningCount > 0 THEN CAST(@WarningCount AS NVARCHAR) + N' Warn, ' ELSE '' END +
            CAST(@HealthyCount AS NVARCHAR) + N' Healthy';

    IF @CriticalCount > 0 SET @Importance = 'High';

    IF @SendEmail = 1 AND @TotalServers > 0
    BEGIN
        EXEC msdb.dbo.sp_send_dbmail
            @profile_name = 'DBAOpsProfile',
            @recipients = @Recipients,
            @subject = @Subject,
            @body = @TableHTML,
            @body_format = 'HTML',
            @importance = @Importance;
        PRINT 'Report sent: ' + @Subject;
    END
    
    SELECT @TotalServers AS TotalServers, @CriticalCount AS Critical, @WarningCount AS Warning, @HealthyCount AS Healthy, @ReportType AS ReportType;
    DROP TABLE #Health;
END;

GO

-- [Alerting].[usp_CheckBackupAlerts]

-- ============================================================================
-- FIX: Alerting.usp_CheckBackupAlerts
-- ============================================================================
CREATE   PROCEDURE Alerting.usp_CheckBackupAlerts
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @Subject NVARCHAR(500);
    DECLARE @Body NVARCHAR(MAX);
    DECLARE @Recipients NVARCHAR(1000) = 'dbaopsmail@YOURORG.gov';
    DECLARE @FailedCount INT = 0;
    
    SELECT @Recipients = ISNULL(JSON_VALUE(ConfigValue, '$.Critical[0]'), 'dbaopsmail@YOURORG.gov')
    FROM Logging.NotificationConfig
    WHERE ConfigName = 'Alert_Recipients';
    
    IF OBJECT_ID('Compliance.BackupCompliance', 'U') IS NOT NULL
    BEGIN
        SELECT @FailedCount = COUNT(*)
        FROM Compliance.BackupCompliance
        WHERE IsCompliant = 0;
        
        IF @FailedCount > 0
        BEGIN
            SET @Subject = 'CRITICAL: ' + CAST(@FailedCount AS VARCHAR(10)) + ' Database(s) Missing Backups';
            SET @Body = CAST(@FailedCount AS VARCHAR(10)) + ' databases are non-compliant for backups. Check the DBAOps dashboard.';
            
            EXEC Alerting.usp_SendEmailAlert
                @Recipients = @Recipients,
                @Subject = @Subject,
                @Body = @Body,
                @Severity = 'Critical',
                @Category = 'Backup';
        END
    END
    
    EXEC Logging.usp_WriteLog
        @LogLevel = 'INFO',
        @Category = 'Alerting',
        @Operation = 'Check Backup Alerts',
        @Message = 'Completed',
        @RowsAffected = @FailedCount;
END

GO

-- [Alerting].[usp_CheckDiskSpaceAlerts]

-- ============================================================================
-- FIX: Alerting.usp_CheckDiskSpaceAlerts
-- ============================================================================
CREATE   PROCEDURE Alerting.usp_CheckDiskSpaceAlerts
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @Subject NVARCHAR(500);
    DECLARE @Body NVARCHAR(MAX);
    DECLARE @Recipients NVARCHAR(1000) = 'dbaopsmail@YOURORG.gov';
    DECLARE @CriticalCount INT = 0;
    
    SELECT @Recipients = ISNULL(JSON_VALUE(ConfigValue, '$.Critical[0]'), 'dbaopsmail@YOURORG.gov')
    FROM Logging.NotificationConfig
    WHERE ConfigName = 'Alert_Recipients';
    
    IF OBJECT_ID('metrics.DiskSpace', 'U') IS NOT NULL
    BEGIN
        SELECT @CriticalCount = COUNT(*)
        FROM metrics.DiskSpace
        WHERE UsedPercent >= 95
          AND CollectedDate >= DATEADD(HOUR, -2, GETDATE());
        
        IF @CriticalCount > 0
        BEGIN
            SET @Subject = 'CRITICAL: ' + CAST(@CriticalCount AS VARCHAR(10)) + ' Drive(s) Above 95%';
            SET @Body = CAST(@CriticalCount AS VARCHAR(10)) + ' drives are critically low on space. Immediate action required.';
            
            EXEC Alerting.usp_SendEmailAlert
                @Recipients = @Recipients,
                @Subject = @Subject,
                @Body = @Body,
                @Severity = 'Critical',
                @Category = 'DiskSpace';
        END
    END
    
    EXEC Logging.usp_WriteLog
        @LogLevel = 'INFO',
        @Category = 'Alerting',
        @Operation = 'Check Disk Space',
        @Message = 'Completed',
        @RowsAffected = @CriticalCount;
END

GO

-- [Alerting].[usp_CheckJobFailureAlerts]

-- ============================================================================
-- FIX: Alerting.usp_CheckJobFailureAlerts
-- ============================================================================
CREATE   PROCEDURE Alerting.usp_CheckJobFailureAlerts
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @Subject NVARCHAR(500);
    DECLARE @Body NVARCHAR(MAX);
    DECLARE @Recipients NVARCHAR(1000) = 'dbaopsmail@YOURORG.gov';
    DECLARE @FailedCount INT = 0;
    
    -- Get recipients from config
    SELECT @Recipients = ISNULL(JSON_VALUE(ConfigValue, '$.Critical[0]'), 'dbaopsmail@YOURORG.gov')
    FROM Logging.NotificationConfig
    WHERE ConfigName = 'Alert_Recipients';
    
    -- Check for job failures
    IF OBJECT_ID('jobs.SQLAgentJobs', 'U') IS NOT NULL
    BEGIN
        SELECT @FailedCount = COUNT(*)
        FROM jobs.SQLAgentJobs
        WHERE LastRunOutcome = 'Failed'
          AND LastRunDate >= DATEADD(HOUR, -1, GETDATE());
        
        IF @FailedCount > 0
        BEGIN
            SET @Subject = 'WARNING: ' + CAST(@FailedCount AS VARCHAR(10)) + ' SQL Agent Job(s) Failed';
            SET @Body = CAST(@FailedCount AS VARCHAR(10)) + ' SQL Agent jobs failed in the last hour. Check the DBAOps dashboard for details.';
            
            EXEC Alerting.usp_SendEmailAlert
                @Recipients = @Recipients,
                @Subject = @Subject,
                @Body = @Body,
                @Severity = 'Warning',
                @Category = 'JobFailure';
        END
    END
    
    EXEC Logging.usp_WriteLog
        @LogLevel = 'INFO',
        @Category = 'Alerting',
        @Operation = 'Check Job Failures',
        @Message = 'Completed',
        @RowsAffected = @FailedCount;
END

GO

-- [Alerting].[usp_ProcessAllAlerts]

-- ============================================================================
-- FIX: Alerting.usp_ProcessAllAlerts
-- ============================================================================
CREATE   PROCEDURE Alerting.usp_ProcessAllAlerts
AS
BEGIN
    SET NOCOUNT ON;
    
    EXEC Logging.usp_WriteLog
        @LogLevel = 'INFO',
        @Category = 'Alerting',
        @Operation = 'Process All Alerts',
        @Message = 'Starting';
    
    BEGIN TRY
        EXEC Alerting.usp_CheckBackupAlerts;
        EXEC Alerting.usp_CheckDiskSpaceAlerts;
        EXEC Alerting.usp_CheckJobFailureAlerts;
        
        EXEC Logging.usp_WriteLog
            @LogLevel = 'INFO',
            @Category = 'Alerting',
            @Operation = 'Process All Alerts',
            @Message = 'Completed';
    END TRY
    BEGIN CATCH
        DECLARE @ErrMsg NVARCHAR(2000) = ERROR_MESSAGE();
        EXEC Logging.usp_WriteLog
            @LogLevel = 'ERROR',
            @Category = 'Alerting',
            @Operation = 'Process All Alerts',
            @Message = @ErrMsg;
    END CATCH
END

GO

-- [Alerting].[usp_SendEmailAlert]

-- ============================================================================
-- FIX: Alerting.usp_SendEmailAlert - Ultra Simple
-- ============================================================================
CREATE   PROCEDURE Alerting.usp_SendEmailAlert
    @Recipients NVARCHAR(1000),
    @Subject NVARCHAR(500),
    @Body NVARCHAR(MAX),
    @Severity VARCHAR(20) = 'Info',
    @Category VARCHAR(50) = 'General',
    @ServerName NVARCHAR(128) = NULL,
    @DatabaseName NVARCHAR(128) = NULL,
    @SourceAlertRule NVARCHAR(128) = NULL,
    @Importance VARCHAR(10) = 'Normal'
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @AlertID BIGINT;
    DECLARE @ProfileName NVARCHAR(128);
    
    -- Get Database Mail profile
    SELECT TOP 1 @ProfileName = name 
    FROM msdb.dbo.sysmail_profile 
    ORDER BY profile_id;
    
    -- Set importance based on severity
    IF @Severity = 'Critical' SET @Importance = 'High';
    
    -- Log the alert
    EXEC Logging.usp_WriteAlert
        @AlertType = 'Email',
        @Severity = @Severity,
        @Category = @Category,
        @Subject = @Subject,
        @Message = @Body,
        @ServerName = @ServerName,
        @DatabaseName = @DatabaseName,
        @Recipients = @Recipients,
        @SourceAlertRule = @SourceAlertRule,
        @AlertID = @AlertID OUTPUT;
    
    -- Send the email (plain text to avoid HTML issues)
    BEGIN TRY
        IF @ProfileName IS NOT NULL
        BEGIN
            DECLARE @FullBody NVARCHAR(MAX);
            SET @FullBody = 'DBAOps Alert: ' + @Subject + CHAR(13) + CHAR(10)
                + '----------------------------------------' + CHAR(13) + CHAR(10)
                + 'Severity: ' + @Severity + CHAR(13) + CHAR(10)
                + 'Category: ' + @Category + CHAR(13) + CHAR(10)
                + 'Time: ' + CONVERT(VARCHAR(30), GETDATE(), 120) + CHAR(13) + CHAR(10);
            
            IF @ServerName IS NOT NULL
                SET @FullBody = @FullBody + 'Server: ' + @ServerName + CHAR(13) + CHAR(10);
            
            IF @DatabaseName IS NOT NULL
                SET @FullBody = @FullBody + 'Database: ' + @DatabaseName + CHAR(13) + CHAR(10);
            
            SET @FullBody = @FullBody + '----------------------------------------' + CHAR(13) + CHAR(10)
                + @Body + CHAR(13) + CHAR(10)
                + '----------------------------------------' + CHAR(13) + CHAR(10)
                + 'DBAOps Monitoring System | YOURSERVER01';
            
            EXEC msdb.dbo.sp_send_dbmail
                @profile_name = @ProfileName,
                @recipients = @Recipients,
                @subject = @Subject,
                @body = @FullBody,
                @importance = @Importance;
            
            EXEC Logging.usp_UpdateAlertStatus @AlertID = @AlertID, @DeliveryStatus = 'Sent';
        END
        ELSE
        BEGIN
            EXEC Logging.usp_UpdateAlertStatus 
                @AlertID = @AlertID, 
                @DeliveryStatus = 'Failed',
                @ErrorMessage = 'No Database Mail profile configured';
        END
    END TRY
    BEGIN CATCH
        DECLARE @ErrMsg NVARCHAR(2000) = ERROR_MESSAGE();
        EXEC Logging.usp_UpdateAlertStatus 
            @AlertID = @AlertID, 
            @DeliveryStatus = 'Failed',
            @ErrorMessage = @ErrMsg;
    END CATCH
END

GO

-- [Alerting].[usp_SendTeamsAlert]

-- ============================================================================
-- FIX: Alerting.usp_SendTeamsAlert - Simplified
-- ============================================================================
CREATE   PROCEDURE Alerting.usp_SendTeamsAlert
    @Subject NVARCHAR(500),
    @Message NVARCHAR(MAX),
    @Severity VARCHAR(20) = 'Info',
    @Category VARCHAR(50) = 'General',
    @ServerName NVARCHAR(128) = NULL,
    @DatabaseName NVARCHAR(128) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @AlertID BIGINT;
    DECLARE @WebhookUrl NVARCHAR(1000);
    DECLARE @IsEnabled BIT = 0;
    
    -- Check if Teams is enabled
    SELECT 
        @WebhookUrl = JSON_VALUE(ConfigValue, '$.WebhookUrl'),
        @IsEnabled = IsEnabled
    FROM Logging.NotificationConfig 
    WHERE ConfigName = 'Teams_Webhook';
    
    -- Skip if not configured
    IF @IsEnabled = 0 OR @WebhookUrl IS NULL OR @WebhookUrl LIKE '%YOUR-WEBHOOK%'
    BEGIN
        RETURN;
    END
    
    -- Log the alert
    EXEC Logging.usp_WriteAlert
        @AlertType = 'Teams',
        @Severity = @Severity,
        @Category = @Category,
        @Subject = @Subject,
        @Message = @Message,
        @ServerName = @ServerName,
        @DatabaseName = @DatabaseName,
        @Recipients = 'Teams Channel',
        @AlertID = @AlertID OUTPUT;
    
    -- Execute PowerShell (simplified command)
    DECLARE @cmd NVARCHAR(2000);
    DECLARE @CleanSubject NVARCHAR(200);
    DECLARE @CleanMessage NVARCHAR(200);
    
    SET @CleanSubject = REPLACE(REPLACE(LEFT(@Subject, 100), '"', ''), '''', '');
    SET @CleanMessage = REPLACE(REPLACE(LEFT(@Message, 100), '"', ''), '''', '');
    
    SET @cmd = 'powershell -File "E:\DBAOps\Scripts\Send-TeamsAlert.ps1" -Subject "' 
        + @CleanSubject + '" -Message "' + @CleanMessage + '" -Severity "' + @Severity + '"';
    
    BEGIN TRY
        DECLARE @result INT;
        EXEC @result = xp_cmdshell @cmd, NO_OUTPUT;
        EXEC Logging.usp_UpdateAlertStatus @AlertID = @AlertID, @DeliveryStatus = 'Sent';
    END TRY
    BEGIN CATCH
        EXEC Logging.usp_UpdateAlertStatus @AlertID = @AlertID, @DeliveryStatus = 'Failed';
    END CATCH
END

GO

-- [Assessment].[usp_CleanupHistory]

CREATE PROCEDURE Assessment.usp_CleanupHistory
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @RetentionDays INT;
    SELECT @RetentionDays = CAST(ConfigValue AS INT) FROM Assessment.Configuration WHERE ConfigKey = 'RetentionDays';
    SET @RetentionDays = ISNULL(@RetentionDays, 365);
    
    DECLARE @CutoffDate DATETIME2 = DATEADD(DAY, -@RetentionDays, SYSDATETIME());
    DECLARE @DeletedCount INT;
    
    DELETE FROM Assessment.AssessmentHistory
    WHERE AssessmentDate < @CutoffDate;
    
    SET @DeletedCount = @@ROWCOUNT;
    
    IF @DeletedCount > 0
        PRINT 'Deleted ' + CAST(@DeletedCount AS VARCHAR(10)) + ' assessment history records older than ' + CAST(@RetentionDays AS VARCHAR(10)) + ' days.';
END

GO

-- [Assessment].[usp_GenerateDailyStatus]

/*===========================================================
  2. STATUS GENERATOR PROCEDURE
===========================================================*/
CREATE   PROCEDURE Assessment.usp_GenerateDailyStatus
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE
        @ServerName SYSNAME = @@SERVERNAME,
        @Critical INT,
        @High INT,
        @Medium INT,
        @Low INT,
        @Maturity DECIMAL(5,2),
        @Message NVARCHAR(1000);

    /* Defensive counts (safe even if tables evolve) */
    SELECT
        @Critical = COUNT(*) FROM Assessment.ArchitectureFindings WHERE Severity = 'CRITICAL';

    SELECT
        @High = COUNT(*) FROM Assessment.ArchitectureFindings WHERE Severity = 'HIGH';

    SELECT
        @Medium = COUNT(*) FROM Assessment.ArchitectureFindings WHERE Severity = 'MEDIUM';

    SELECT
        @Low = COUNT(*) FROM Assessment.ArchitectureFindings WHERE Severity = 'LOW';

    /* Simple maturity model */
    SET @Maturity =
        CASE
            WHEN (@Critical + @High + @Medium + @Low) = 0 THEN 100
            ELSE
                100.0 -
                (
                    ( @Critical * 2.0 + @High * 1.5 + @Medium * 1.0 + @Low * 0.5 )
                    /
                    NULLIF((@Critical + @High + @Medium + @Low) * 2.0,0)
                ) * 100
        END;

    SET @Message =
        'DBAOps Daily Architecture Status: ' +
        CASE
            WHEN @Critical > 0 THEN 'CRITICAL issues detected'
            WHEN @High > 0 THEN 'High-risk issues detected'
            ELSE 'No critical or high-risk issues detected'
        END +
        ' on ' + @ServerName +
        ' with a maturity score of ' + CAST(@Maturity AS VARCHAR(10)) + '%, ' +
        CAST(@Critical AS VARCHAR(10)) + ' critical, ' +
        CAST(@High AS VARCHAR(10)) + ' high, ' +
        CAST(@Medium AS VARCHAR(10)) + ' medium, and ' +
        CAST(@Low AS VARCHAR(10)) + ' low findings as of ' +
        CONVERT(VARCHAR(30), SYSDATETIME(), 120) + '.';

    INSERT INTO Assessment.DailyStatusSummary (ServerName, StatusMessage)
    VALUES (@ServerName, @Message);

    SELECT @Message AS DailyStatusReport;
END

GO

-- [Assessment].[usp_RunArchitectureAssessment]

CREATE PROCEDURE Assessment.usp_RunArchitectureAssessment
    @SendNotification BIT = 1,
    @ForceNotification BIT = 0
AS
BEGIN
    SET NOCOUNT ON;
    SET XACT_ABORT ON;
    
    DECLARE @StartTime DATETIME2 = SYSDATETIME();
    DECLARE @AssessmentDate DATETIME2 = SYSDATETIME();
    
    -- Variables for metrics
    DECLARE @TotalSchemas INT = 0;
    DECLARE @TotalTables INT = 0;
    DECLARE @TotalStoredProcs INT = 0;
    DECLARE @TotalViews INT = 0;
    DECLARE @TotalFunctions INT = 0;
    DECLARE @TotalTriggers INT = 0;
    DECLARE @TotalJobs INT = 0;
    DECLARE @TotalLinkedServers INT = 0;
    DECLARE @TotalRowCount BIGINT = 0;
    DECLARE @DatabaseSizeMB DECIMAL(18,2) = 0;
    
    -- Variables for findings
    DECLARE @CriticalCount INT = 0;
    DECLARE @HighCount INT = 0;
    DECLARE @MediumCount INT = 0;
    DECLARE @LowCount INT = 0;
    DECLARE @TotalFindings INT = 0;
    DECLARE @TotalEffort DECIMAL(10,2) = 0;
    DECLARE @MaturityScore DECIMAL(5,2) = 0;
    DECLARE @MaturityLevel NVARCHAR(50);
    
    -- Previous assessment for comparison
    DECLARE @PreviousScore DECIMAL(5,2);
    DECLARE @PreviousCritical INT;
    
    BEGIN TRY
        -- ====================================================================
        -- Clear previous findings
        -- ====================================================================
        TRUNCATE TABLE Assessment.ArchitectureFindings;
        
        -- ====================================================================
        -- COLLECT METRICS
        -- ====================================================================
        
        -- Database size
        SELECT @DatabaseSizeMB = CAST(SUM(size) * 8.0 / 1024 AS DECIMAL(18,2))
        FROM sys.database_files;
        
        -- Schema count
        SELECT @TotalSchemas = COUNT(*)
        FROM sys.schemas 
        WHERE name NOT IN ('sys', 'INFORMATION_SCHEMA', 'guest', 'db_owner', 'db_accessadmin', 
                           'db_securityadmin', 'db_ddladmin', 'db_backupoperator', 'db_datareader', 
                           'db_datawriter', 'db_denydatareader', 'db_denydatawriter');
        
        -- Table count
        SELECT @TotalTables = COUNT(*) FROM sys.tables WHERE is_ms_shipped = 0;
        
        -- Stored procedure count
        SELECT @TotalStoredProcs = COUNT(*) FROM sys.procedures WHERE is_ms_shipped = 0;
        
        -- View count
        SELECT @TotalViews = COUNT(*) FROM sys.views WHERE is_ms_shipped = 0;
        
        -- Function count
        SELECT @TotalFunctions = COUNT(*) FROM sys.objects WHERE type IN ('FN', 'IF', 'TF') AND is_ms_shipped = 0;
        
        -- Trigger count
        SELECT @TotalTriggers = COUNT(*) FROM sys.triggers WHERE parent_class = 1 AND is_ms_shipped = 0;
        
        -- SQL Agent job count
        SELECT @TotalJobs = COUNT(*) FROM msdb.dbo.sysjobs;
        
        -- Linked server count
        SELECT @TotalLinkedServers = COUNT(*) FROM sys.servers WHERE server_id > 0;
        
        -- Total row count
        SELECT @TotalRowCount = ISNULL(SUM(p.rows), 0)
        FROM sys.tables t
        INNER JOIN sys.indexes i ON t.object_id = i.object_id AND i.index_id IN (0,1)
        INNER JOIN sys.partitions p ON i.object_id = p.object_id AND i.index_id = p.index_id
        WHERE t.is_ms_shipped = 0;
        
        -- ====================================================================
        -- PERFORM GAP ANALYSIS
        -- ====================================================================
        
        -- Check for DBAOps schema
        IF NOT EXISTS (SELECT 1 FROM sys.schemas WHERE name = 'DBAOps')
        BEGIN
            INSERT INTO Assessment.ArchitectureFindings 
                (Category, SubCategory, Finding, Severity, CurrentState, RecommendedState, Priority, EstimatedEffortHours)
            VALUES 
                ('Schema Architecture', 'Missing Schema', 'DBAOps schema does not exist', 'CRITICAL',
                 'Schema not found', 'Create DBAOps schema', 1, 0.5);
        END
        
        -- Check for missing critical tables
        DECLARE @ExpectedTables TABLE (SchemaName NVARCHAR(128), TableName NVARCHAR(128), Purpose NVARCHAR(500), Priority INT);
        INSERT INTO @ExpectedTables VALUES
            ('DBAOps', 'ServerInventory', 'Central inventory of all SQL Server instances', 1),
            ('DBAOps', 'DatabaseInventory', 'Complete inventory of all databases', 1),
            ('DBAOps', 'BackupTelemetry', 'Backup job telemetry and validation', 1),
            ('DBAOps', 'WaitStatsHistory', 'Historical wait statistics', 2),
            ('DBAOps', 'JobExecutionHistory', 'SQL Agent job execution tracking', 2),
            ('DBAOps', 'AlertHistory', 'Alert and notification tracking', 2),
            ('DBAOps', 'ComplianceChecks', 'Compliance and security audit results', 2),
            ('DBAOps', 'DiskSpaceHistory', 'Disk space usage trending', 2),
            ('DBAOps', 'ConfigurationBaseline', 'Server configuration baseline', 2),
            ('DBAOps', 'MaintenanceHistory', 'Index and statistics maintenance tracking', 3);
        
        INSERT INTO Assessment.ArchitectureFindings 
            (Category, SubCategory, Finding, Severity, CurrentState, RecommendedState, Priority, EstimatedEffortHours)
        SELECT 
            'Data Architecture', 'Missing Table',
            'Missing DBAOps table: ' + et.SchemaName + '.' + et.TableName,
            CASE et.Priority WHEN 1 THEN 'CRITICAL' WHEN 2 THEN 'HIGH' ELSE 'MEDIUM' END,
            'Table does not exist', 'Create table ' + et.SchemaName + '.' + et.TableName,
            et.Priority, 2.0
        FROM @ExpectedTables et
        WHERE NOT EXISTS (
            SELECT 1 FROM sys.tables t 
            INNER JOIN sys.schemas s ON t.schema_id = s.schema_id
            WHERE s.name = et.SchemaName AND t.name = et.TableName
        );
        
        -- Check for missing critical procedures
        DECLARE @ExpectedProcs TABLE (SchemaName NVARCHAR(128), ProcName NVARCHAR(128), Purpose NVARCHAR(500), Priority INT);
        INSERT INTO @ExpectedProcs VALUES
            ('DBAOps', 'usp_CollectServerInventory', 'Updates server inventory', 1),
            ('DBAOps', 'usp_CollectDatabaseInventory', 'Updates database inventory', 1),
            ('DBAOps', 'usp_CollectPerformanceTelemetry', 'Collects performance metrics', 1),
            ('DBAOps', 'usp_CollectBackupTelemetry', 'Collects backup telemetry', 1),
            ('DBAOps', 'usp_CollectWaitStats', 'Collects wait statistics', 2),
            ('DBAOps', 'usp_MonitorJobExecution', 'Monitors job execution', 2),
            ('DBAOps', 'usp_GenerateHealthReport', 'Generates health reports', 2),
            ('DBAOps', 'usp_ComplianceAudit', 'Performs compliance audits', 2),
            ('DBAOps', 'usp_RetentionCleanup', 'Manages data retention', 3);
        
        INSERT INTO Assessment.ArchitectureFindings 
            (Category, SubCategory, Finding, Severity, CurrentState, RecommendedState, Priority, EstimatedEffortHours)
        SELECT 
            'Automation', 'Missing Procedure',
            'Missing DBAOps procedure: ' + ep.SchemaName + '.' + ep.ProcName,
            CASE ep.Priority WHEN 1 THEN 'CRITICAL' WHEN 2 THEN 'HIGH' ELSE 'MEDIUM' END,
            'Procedure does not exist', 'Create procedure ' + ep.SchemaName + '.' + ep.ProcName,
            ep.Priority + 1, 4.0
        FROM @ExpectedProcs ep
        WHERE NOT EXISTS (
            SELECT 1 FROM sys.procedures p 
            INNER JOIN sys.schemas s ON p.schema_id = s.schema_id
            WHERE s.name = ep.SchemaName AND p.name = ep.ProcName
        );
        
        -- Check for heap tables with significant data
        INSERT INTO Assessment.ArchitectureFindings 
            (Category, SubCategory, Finding, Severity, CurrentState, RecommendedState, Priority, EstimatedEffortHours)
        SELECT 
            'Performance', 'Table Design',
            'Heap table (no clustered index): ' + SCHEMA_NAME(t.schema_id) + '.' + t.name,
            CASE WHEN p.rows > 10000 THEN 'HIGH' ELSE 'MEDIUM' END,
            'Table has ' + CAST(p.rows AS VARCHAR(20)) + ' rows without clustered index',
            'Add appropriate clustered index',
            CASE WHEN p.rows > 10000 THEN 3 ELSE 5 END, 1.0
        FROM sys.tables t
        INNER JOIN sys.indexes i ON t.object_id = i.object_id AND i.index_id = 0
        INNER JOIN sys.partitions p ON i.object_id = p.object_id AND i.index_id = p.index_id
        WHERE t.is_ms_shipped = 0;
        
        -- Check for jobs without schedules
        INSERT INTO Assessment.ArchitectureFindings 
            (Category, SubCategory, Finding, Severity, CurrentState, RecommendedState, Priority, EstimatedEffortHours)
        SELECT 
            'Automation', 'Job Configuration',
            'Enabled job without schedule: ' + j.name,
            'MEDIUM',
            'Job is enabled but has no schedule',
            'Add schedule or disable if not needed',
            5, 0.5
        FROM msdb.dbo.sysjobs j
        WHERE j.[enabled] = 1
          AND NOT EXISTS (SELECT 1 FROM msdb.dbo.sysjobschedules js WHERE js.job_id = j.job_id);
        
        -- Check for failed jobs in last 24 hours
        INSERT INTO Assessment.ArchitectureFindings 
            (Category, SubCategory, Finding, Severity, CurrentState, RecommendedState, Priority, EstimatedEffortHours)
        SELECT DISTINCT
            'Automation', 'Job Failures',
            'Job failed in last 24 hours: ' + j.name,
            'HIGH',
            'Job failed at ' + CONVERT(VARCHAR(30), msdb.dbo.agent_datetime(h.run_date, h.run_time), 120),
            'Investigate and fix the root cause',
            2, 1.0
        FROM msdb.dbo.sysjobhistory h
        INNER JOIN msdb.dbo.sysjobs j ON h.job_id = j.job_id
        WHERE h.run_status = 0
          AND h.step_id = 0
          AND msdb.dbo.agent_datetime(h.run_date, h.run_time) > DATEADD(HOUR, -24, GETDATE());
        
        -- Check database configuration issues
        IF EXISTS (SELECT 1 FROM sys.databases WHERE name = DB_NAME() AND is_auto_shrink_on = 1)
        BEGIN
            INSERT INTO Assessment.ArchitectureFindings 
                (Category, SubCategory, Finding, Severity, CurrentState, RecommendedState, Priority, EstimatedEffortHours)
            VALUES 
                ('Database Configuration', 'Settings', 'Auto Shrink is enabled', 'HIGH',
                 'AUTO_SHRINK = ON', 'Disable AUTO_SHRINK', 1, 0.25);
        END
        
        -- ====================================================================
        -- CALCULATE SCORES
        -- ====================================================================
        SELECT 
            @CriticalCount = ISNULL(SUM(CASE WHEN Severity = 'CRITICAL' THEN 1 ELSE 0 END), 0),
            @HighCount = ISNULL(SUM(CASE WHEN Severity = 'HIGH' THEN 1 ELSE 0 END), 0),
            @MediumCount = ISNULL(SUM(CASE WHEN Severity = 'MEDIUM' THEN 1 ELSE 0 END), 0),
            @LowCount = ISNULL(SUM(CASE WHEN Severity = 'LOW' THEN 1 ELSE 0 END), 0),
            @TotalEffort = ISNULL(SUM(EstimatedEffortHours), 0)
        FROM Assessment.ArchitectureFindings;
        
        SET @TotalFindings = @CriticalCount + @HighCount + @MediumCount + @LowCount;
        
        -- Calculate maturity score (weighted)
        DECLARE @WeightedDeductions DECIMAL(10,2);
        SET @WeightedDeductions = (@CriticalCount * 15.0) + (@HighCount * 8.0) + (@MediumCount * 3.0) + (@LowCount * 1.0);
        SET @MaturityScore = CASE 
            WHEN 100.0 - @WeightedDeductions < 0 THEN 0
            WHEN 100.0 - @WeightedDeductions > 100 THEN 100
            ELSE 100.0 - @WeightedDeductions
        END;
        
        SET @MaturityLevel = CASE 
            WHEN @MaturityScore >= 90 THEN 'ADVANCED - Production Ready'
            WHEN @MaturityScore >= 75 THEN 'MATURE - Minor Enhancements Needed'
            WHEN @MaturityScore >= 60 THEN 'DEVELOPING - Significant Gaps Exist'
            WHEN @MaturityScore >= 40 THEN 'BASIC - Major Work Required'
            ELSE 'INITIAL - Framework Incomplete'
        END;
        
        -- Get previous assessment for comparison
        SELECT TOP 1 
            @PreviousScore = MaturityScore,
            @PreviousCritical = CriticalFindings
        FROM Assessment.AssessmentHistory
        WHERE AssessmentDate < @AssessmentDate
        ORDER BY AssessmentDate DESC;
        
        -- ====================================================================
        -- SAVE TO HISTORY
        -- ====================================================================
        INSERT INTO Assessment.AssessmentHistory (
            AssessmentDate, ServerName, DatabaseName, MaturityScore, MaturityLevel,
            TotalFindings, CriticalFindings, HighFindings, MediumFindings, LowFindings,
            TotalSchemas, TotalTables, TotalStoredProcs, TotalViews, TotalJobs,
            TotalLinkedServers, DatabaseSizeMB, TotalRowCount, EstimatedEffortHours,
            ExecutionDurationSeconds
        )
        VALUES (
            @AssessmentDate, @@SERVERNAME, DB_NAME(), @MaturityScore, @MaturityLevel,
            @TotalFindings, @CriticalCount, @HighCount, @MediumCount, @LowCount,
            @TotalSchemas, @TotalTables, @TotalStoredProcs, @TotalViews, @TotalJobs,
            @TotalLinkedServers, @DatabaseSizeMB, @TotalRowCount, @TotalEffort,
            DATEDIFF(SECOND, @StartTime, SYSDATETIME())
        );
        
        -- ====================================================================
        -- SEND NOTIFICATION IF ENABLED
        -- ====================================================================
        IF @SendNotification = 1
        BEGIN
            DECLARE @ShouldNotify BIT = 0;
            DECLARE @NotificationReason NVARCHAR(500) = '';
            
            -- Check notification conditions
            DECLARE @CriticalThreshold INT;
            DECLARE @ScoreThreshold DECIMAL(5,2);
            DECLARE @EnableNotifications BIT;
            
            SELECT @CriticalThreshold = CAST(ConfigValue AS INT) FROM Assessment.Configuration WHERE ConfigKey = 'CriticalThreshold';
            SELECT @ScoreThreshold = CAST(ConfigValue AS DECIMAL(5,2)) FROM Assessment.Configuration WHERE ConfigKey = 'MaturityScoreThreshold';
            SELECT @EnableNotifications = CAST(ConfigValue AS BIT) FROM Assessment.Configuration WHERE ConfigKey = 'EnableNotifications';
            
            -- Set defaults if config not found
            SET @CriticalThreshold = ISNULL(@CriticalThreshold, 3);
            SET @ScoreThreshold = ISNULL(@ScoreThreshold, 60);
            SET @EnableNotifications = ISNULL(@EnableNotifications, 1);
            
            IF @EnableNotifications = 1
            BEGIN
                -- Force notification
                IF @ForceNotification = 1
                BEGIN
                    SET @ShouldNotify = 1;
                    SET @NotificationReason = 'Scheduled Assessment';
                END
                -- Critical threshold exceeded
                ELSE IF @CriticalCount >= @CriticalThreshold
                BEGIN
                    SET @ShouldNotify = 1;
                    SET @NotificationReason = 'Critical threshold exceeded (' + CAST(@CriticalCount AS VARCHAR(10)) + ' critical findings)';
                END
                -- Score below threshold
                ELSE IF @MaturityScore < @ScoreThreshold
                BEGIN
                    SET @ShouldNotify = 1;
                    SET @NotificationReason = 'Maturity score below threshold (' + CAST(@MaturityScore AS VARCHAR(10)) + '%)';
                END
                -- Score degraded
                ELSE IF @PreviousScore IS NOT NULL AND @MaturityScore < @PreviousScore - 5
                BEGIN
                    SET @ShouldNotify = 1;
                    SET @NotificationReason = 'Score degraded from ' + CAST(@PreviousScore AS VARCHAR(10)) + '% to ' + CAST(@MaturityScore AS VARCHAR(10)) + '%';
                END
            END
            
            IF @ShouldNotify = 1
            BEGIN
                EXEC Assessment.usp_SendAssessmentNotification 
                    @AssessmentDate = @AssessmentDate,
                    @NotificationReason = @NotificationReason;
            END
        END
        
        -- Return summary
        SELECT 
            'Assessment Complete' AS [Status],
            @MaturityScore AS MaturityScore,
            @MaturityLevel AS MaturityLevel,
            @TotalFindings AS TotalFindings,
            @CriticalCount AS CriticalFindings,
            @HighCount AS HighFindings,
            @TotalEffort AS EstimatedEffortHours,
            DATEDIFF(SECOND, @StartTime, SYSDATETIME()) AS ExecutionSeconds;
            
    END TRY
    BEGIN CATCH
        -- Log error
        INSERT INTO Assessment.ArchitectureFindings 
            (Category, SubCategory, Finding, Severity, CurrentState, Priority, EstimatedEffortHours)
        VALUES 
            ('System', 'Error', 'Assessment failed: ' + ERROR_MESSAGE(), 'CRITICAL',
             'Error at line ' + CAST(ERROR_LINE() AS VARCHAR(10)), 1, 0);
        
        THROW;
    END CATCH
END

GO

-- [Assessment].[usp_RunComplianceAssessment]
-- Fix and recreate the procedure with correct column names
CREATE   PROCEDURE Assessment.usp_RunComplianceAssessment
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @AssessmentDate DATETIME2 = SYSDATETIME();
    DECLARE @ServerName VARCHAR(128) = @@SERVERNAME;
    
    -- Clear previous results for today
    DELETE FROM Assessment.ComplianceControlResults 
    WHERE CAST(AssessmentDate AS DATE) = CAST(@AssessmentDate AS DATE);
    
    -- =============================================
    -- STIG AUTOMATED COMPLIANCE CHECKS
    -- =============================================
    
    -- STIG-V1: Windows Authentication Mode
    INSERT INTO Assessment.ComplianceControlResults (AssessmentDate, FrameworkCode, ControlCode, ServerName, ControlStatus, Finding, Evidence)
    SELECT @AssessmentDate, 'STIG', 'STIG-V1', @ServerName,
        CASE WHEN SERVERPROPERTY('IsIntegratedSecurityOnly') = 1 THEN 'PASS' ELSE 'FAIL' END,
        CASE WHEN SERVERPROPERTY('IsIntegratedSecurityOnly') = 1 
            THEN 'Windows Authentication Only mode is enabled'
            ELSE 'Mixed Mode authentication is enabled - security risk' END,
        'IsIntegratedSecurityOnly = ' + CAST(ISNULL(SERVERPROPERTY('IsIntegratedSecurityOnly'), 0) AS VARCHAR);
    
    -- STIG-V2: SA Account Status
    INSERT INTO Assessment.ComplianceControlResults (AssessmentDate, FrameworkCode, ControlCode, ServerName, ControlStatus, Finding, Evidence)
    SELECT @AssessmentDate, 'STIG', 'STIG-V2', @ServerName,
        CASE WHEN is_disabled = 1 THEN 'PASS' ELSE 'FAIL' END,
        CASE WHEN is_disabled = 1 
            THEN 'SA account is properly disabled'
            ELSE 'SA account is ENABLED - Critical security risk' END,
        'SA is_disabled = ' + CAST(is_disabled AS VARCHAR)
    FROM sys.server_principals WHERE name = 'sa';
    
    -- STIG-V3: Audit Login Failures
    INSERT INTO Assessment.ComplianceControlResults (AssessmentDate, FrameworkCode, ControlCode, ServerName, ControlStatus, Finding, Evidence)
    SELECT @AssessmentDate, 'STIG', 'STIG-V3', @ServerName,
        CASE WHEN CAST(value_in_use AS INT) = 1 THEN 'PASS' ELSE 'FAIL' END,
        CASE WHEN CAST(value_in_use AS INT) = 1
            THEN 'Default trace is enabled for login auditing'
            ELSE 'Default trace not enabled' END,
        'default trace enabled = ' + CAST(value_in_use AS VARCHAR)
    FROM sys.configurations WHERE name = 'default trace enabled';
    
    -- STIG-V6: CLR Enabled
    INSERT INTO Assessment.ComplianceControlResults (AssessmentDate, FrameworkCode, ControlCode, ServerName, ControlStatus, Finding, Evidence)
    SELECT @AssessmentDate, 'STIG', 'STIG-V6', @ServerName,
        CASE WHEN CAST(value_in_use AS INT) = 0 THEN 'PASS' ELSE 'FAIL' END,
        CASE WHEN CAST(value_in_use AS INT) = 0
            THEN 'CLR is properly disabled'
            ELSE 'CLR is enabled - review if required' END,
        'clr enabled = ' + CAST(value_in_use AS VARCHAR)
    FROM sys.configurations WHERE name = 'clr enabled';
    
    -- STIG-V7: xp_cmdshell
    INSERT INTO Assessment.ComplianceControlResults (AssessmentDate, FrameworkCode, ControlCode, ServerName, ControlStatus, Finding, Evidence)
    SELECT @AssessmentDate, 'STIG', 'STIG-V7', @ServerName,
        CASE WHEN CAST(value_in_use AS INT) = 0 THEN 'PASS' ELSE 'FAIL' END,
        CASE WHEN CAST(value_in_use AS INT) = 0
            THEN 'xp_cmdshell is properly disabled'
            ELSE 'xp_cmdshell is ENABLED - Critical security risk' END,
        'xp_cmdshell = ' + CAST(value_in_use AS VARCHAR)
    FROM sys.configurations WHERE name = 'xp_cmdshell';
    
    -- STIG-V8: Remote Access
    INSERT INTO Assessment.ComplianceControlResults (AssessmentDate, FrameworkCode, ControlCode, ServerName, ControlStatus, Finding, Evidence)
    SELECT @AssessmentDate, 'STIG', 'STIG-V8', @ServerName,
        CASE WHEN CAST(value_in_use AS INT) = 0 THEN 'PASS' ELSE 'FAIL' END,
        CASE WHEN CAST(value_in_use AS INT) = 0
            THEN 'Remote access is properly disabled'
            ELSE 'Remote access is enabled' END,
        'remote access = ' + CAST(value_in_use AS VARCHAR)
    FROM sys.configurations WHERE name = 'remote access';
    
    -- STIG-V9: Cross DB Ownership Chaining
    INSERT INTO Assessment.ComplianceControlResults (AssessmentDate, FrameworkCode, ControlCode, ServerName, ControlStatus, Finding, Evidence)
    SELECT @AssessmentDate, 'STIG', 'STIG-V9', @ServerName,
        CASE WHEN CAST(value_in_use AS INT) = 0 THEN 'PASS' ELSE 'FAIL' END,
        CASE WHEN CAST(value_in_use AS INT) = 0
            THEN 'Cross DB ownership chaining is properly disabled'
            ELSE 'Cross DB ownership chaining is enabled' END,
        'cross db ownership chaining = ' + CAST(value_in_use AS VARCHAR)
    FROM sys.configurations WHERE name = 'cross db ownership chaining';
    
    -- =============================================
    -- NIST AUTOMATED COMPLIANCE CHECKS
    -- =============================================
    
    -- NIST-PR.DS: Data Security (Backup Compliance)
    INSERT INTO Assessment.ComplianceControlResults (AssessmentDate, FrameworkCode, ControlCode, ServerName, ControlStatus, Finding, Evidence)
    SELECT @AssessmentDate, 'NIST', 'NIST-PR.DS', @ServerName,
        CASE WHEN ISNULL(NonCompliantCount, 0) = 0 THEN 'PASS' ELSE 'FAIL' END,
        CASE WHEN ISNULL(NonCompliantCount, 0) = 0 
            THEN 'All databases have recent backups'
            ELSE CAST(NonCompliantCount AS VARCHAR) + ' databases missing backup compliance' END,
        'Non-compliant databases: ' + CAST(ISNULL(NonCompliantCount, 0) AS VARCHAR)
    FROM (SELECT COUNT(*) AS NonCompliantCount FROM fact.BackupCompliance 
          WHERE IsCompliant = 0 AND LoadDate >= DATEADD(DAY, -1, GETDATE())) x;
    
    -- NIST-DE.CM: Security Monitoring (Job Monitoring)
    INSERT INTO Assessment.ComplianceControlResults (AssessmentDate, FrameworkCode, ControlCode, ServerName, ControlStatus, Finding, Evidence)
    SELECT @AssessmentDate, 'NIST', 'NIST-DE.CM', @ServerName,
        CASE WHEN FailedCount < 5 THEN 'PASS' ELSE 'FAIL' END,
        CASE WHEN FailedCount < 5 
            THEN 'Job monitoring is healthy with ' + CAST(FailedCount AS VARCHAR) + ' failures'
            ELSE CAST(FailedCount AS VARCHAR) + ' job failures detected in 24h' END,
        'Failed jobs (24h): ' + CAST(FailedCount AS VARCHAR)
    FROM (SELECT COUNT(*) AS FailedCount FROM msdb.dbo.sysjobhistory h
          JOIN msdb.dbo.sysjobs j ON h.job_id = j.job_id
          WHERE h.step_id = 0 AND h.run_status = 0
          AND msdb.dbo.agent_datetime(h.run_date, h.run_time) >= DATEADD(HOUR, -24, GETDATE())) x;
    
    -- NIST-ID.AM: Asset Management (Server Inventory)
    INSERT INTO Assessment.ComplianceControlResults (AssessmentDate, FrameworkCode, ControlCode, ServerName, ControlStatus, Finding, Evidence)
    SELECT @AssessmentDate, 'NIST', 'NIST-ID.AM', @ServerName,
        CASE WHEN ServerCount > 0 THEN 'PASS' ELSE 'FAIL' END,
        CASE WHEN ServerCount > 0 
            THEN CAST(ServerCount AS VARCHAR) + ' servers in inventory'
            ELSE 'No server inventory data' END,
        'Server inventory count: ' + CAST(ServerCount AS VARCHAR)
    FROM (SELECT COUNT(*) AS ServerCount FROM config.ServerInventory) x;
    
    -- =============================================
    -- CJIS AUTOMATED COMPLIANCE CHECKS
    -- =============================================
    
    -- CJIS-5.4: Auditing and Accountability
    INSERT INTO Assessment.ComplianceControlResults (AssessmentDate, FrameworkCode, ControlCode, ServerName, ControlStatus, Finding, Evidence)
    SELECT @AssessmentDate, 'CJIS', 'CJIS-5.4', @ServerName,
        CASE WHEN EventCount > 0 THEN 'PASS' ELSE 'FAIL' END,
        CASE WHEN EventCount > 0 
            THEN 'DDL auditing is active with ' + CAST(EventCount AS VARCHAR) + ' events captured'
            ELSE 'No DDL audit events found - auditing may not be configured' END,
        'DDL events (30d): ' + CAST(EventCount AS VARCHAR)
    FROM (SELECT COUNT(*) AS EventCount FROM dbo.DDL_AuditEvents
          WHERE EventTime >= DATEADD(DAY, -30, GETDATE())) x;
    
    -- CJIS-5.5: Access Control (Login Audit) - FIXED column name
    INSERT INTO Assessment.ComplianceControlResults (AssessmentDate, FrameworkCode, ControlCode, ServerName, ControlStatus, Finding, Evidence)
    SELECT @AssessmentDate, 'CJIS', 'CJIS-5.5', @ServerName,
        CASE WHEN AuditCount > 0 THEN 'PASS' ELSE 'FAIL' END,
        CASE WHEN AuditCount > 0 
            THEN 'Login auditing active with ' + CAST(AuditCount AS VARCHAR) + ' entries'
            ELSE 'No login audit data found' END,
        'Login audit entries: ' + CAST(AuditCount AS VARCHAR)
    FROM (SELECT COUNT(*) AS AuditCount FROM security.LoginAudit
          WHERE CollectedDate >= DATEADD(DAY, -7, GETDATE())) x;
    
    -- CJIS-5.7: Configuration Management - FIXED column name
    INSERT INTO Assessment.ComplianceControlResults (AssessmentDate, FrameworkCode, ControlCode, ServerName, ControlStatus, Finding, Evidence)
    SELECT @AssessmentDate, 'CJIS', 'CJIS-5.7', @ServerName,
        CASE WHEN ConfigCount > 0 THEN 'PASS' ELSE 'FAIL' END,
        CASE WHEN ConfigCount > 0 
            THEN CAST(ConfigCount AS VARCHAR) + ' configuration changes tracked'
            ELSE 'No configuration baseline tracking' END,
        'Config changes tracked: ' + CAST(ConfigCount AS VARCHAR)
    FROM (SELECT COUNT(*) AS ConfigCount FROM change.ConfigurationChanges
          WHERE ChangeTime >= DATEADD(DAY, -30, GETDATE())) x;
    
    -- =============================================
    -- DISA AUTOMATED COMPLIANCE CHECKS
    -- =============================================
    
    -- DISA-SC1: Encrypted Connections
    INSERT INTO Assessment.ComplianceControlResults (AssessmentDate, FrameworkCode, ControlCode, ServerName, ControlStatus, Finding, Evidence)
    SELECT @AssessmentDate, 'DISA', 'DISA-SC1', @ServerName,
        CASE WHEN encrypt_option = 'TRUE' THEN 'PASS' ELSE 'FAIL' END,
        CASE WHEN encrypt_option = 'TRUE'
            THEN 'Connection encryption is enabled'
            ELSE 'Connection encryption is not enforced' END,
        'encrypt_option = ' + ISNULL(encrypt_option, 'FALSE')
    FROM sys.dm_exec_connections WHERE session_id = @@SPID;
    
    -- DISA-AU1: Audit Configuration
    INSERT INTO Assessment.ComplianceControlResults (AssessmentDate, FrameworkCode, ControlCode, ServerName, ControlStatus, Finding, Evidence)
    SELECT @AssessmentDate, 'DISA', 'DISA-AU1', @ServerName,
        CASE WHEN AuditCount > 0 THEN 'PASS' ELSE 'FAIL' END,
        CASE WHEN AuditCount > 0 
            THEN CAST(AuditCount AS VARCHAR) + ' server audit(s) configured'
            ELSE 'No server audits configured' END,
        'Server audits: ' + CAST(AuditCount AS VARCHAR)
    FROM (SELECT COUNT(*) AS AuditCount FROM sys.server_audits WHERE is_state_enabled = 1) x;
    
    -- DISA-AC1: Account Management
    INSERT INTO Assessment.ComplianceControlResults (AssessmentDate, FrameworkCode, ControlCode, ServerName, ControlStatus, Finding, Evidence)
    SELECT @AssessmentDate, 'DISA', 'DISA-AC1', @ServerName,
        CASE WHEN LoginCount > 0 THEN 'PASS' ELSE 'FAIL' END,
        CASE WHEN LoginCount > 0 
            THEN CAST(LoginCount AS VARCHAR) + ' logins managed'
            ELSE 'No login management data' END,
        'Managed logins: ' + CAST(LoginCount AS VARCHAR)
    FROM (SELECT COUNT(*) AS LoginCount FROM sys.server_principals WHERE type IN ('S', 'U', 'G')) x;
    
    -- DISA-CM1: Configuration Settings
    INSERT INTO Assessment.ComplianceControlResults (AssessmentDate, FrameworkCode, ControlCode, ServerName, ControlStatus, Finding, Evidence)
    SELECT @AssessmentDate, 'DISA', 'DISA-CM1', @ServerName,
        CASE WHEN ComplianceCount > 0 THEN 'PASS' ELSE 'FAIL' END,
        CASE WHEN ComplianceCount > 0 
            THEN CAST(ComplianceCount AS VARCHAR) + ' server config compliance records'
            ELSE 'No configuration compliance tracking' END,
        'Config compliance records: ' + CAST(ComplianceCount AS VARCHAR)
    FROM (SELECT COUNT(*) AS ComplianceCount FROM fact.ServerConfigCompliance) x;
    
    -- =============================================
    -- CALCULATE SCORES BY FRAMEWORK
    -- =============================================
    
    INSERT INTO Assessment.ComplianceScoreHistory (
        AssessmentDate, FrameworkCode, TotalControls, PassedControls, 
        FailedControls, NotApplicable, ComplianceScore, ComplianceGrade, PreviousScore, ScoreChange
    )
    SELECT 
        @AssessmentDate,
        FrameworkCode,
        COUNT(*) AS TotalControls,
        SUM(CASE WHEN ControlStatus = 'PASS' THEN 1 ELSE 0 END) AS PassedControls,
        SUM(CASE WHEN ControlStatus = 'FAIL' THEN 1 ELSE 0 END) AS FailedControls,
        SUM(CASE WHEN ControlStatus = 'N/A' THEN 1 ELSE 0 END) AS NotApplicable,
        CAST(SUM(CASE WHEN ControlStatus = 'PASS' THEN 1 ELSE 0 END) * 100.0 / NULLIF(COUNT(*), 0) AS DECIMAL(5,2)) AS ComplianceScore,
        CASE 
            WHEN SUM(CASE WHEN ControlStatus = 'PASS' THEN 1 ELSE 0 END) * 100.0 / NULLIF(COUNT(*), 0) >= 95 THEN 'A'
            WHEN SUM(CASE WHEN ControlStatus = 'PASS' THEN 1 ELSE 0 END) * 100.0 / NULLIF(COUNT(*), 0) >= 85 THEN 'B'
            WHEN SUM(CASE WHEN ControlStatus = 'PASS' THEN 1 ELSE 0 END) * 100.0 / NULLIF(COUNT(*), 0) >= 75 THEN 'C'
            WHEN SUM(CASE WHEN ControlStatus = 'PASS' THEN 1 ELSE 0 END) * 100.0 / NULLIF(COUNT(*), 0) >= 65 THEN 'D'
            ELSE 'F'
        END AS ComplianceGrade,
        (SELECT TOP 1 ComplianceScore FROM Assessment.ComplianceScoreHistory h2 
         WHERE h2.FrameworkCode = r.FrameworkCode AND h2.AssessmentDate < @AssessmentDate 
         ORDER BY AssessmentDate DESC) AS PreviousScore,
        0 AS ScoreChange
    FROM Assessment.ComplianceControlResults r
    WHERE CAST(AssessmentDate AS DATE) = CAST(@AssessmentDate AS DATE)
    GROUP BY FrameworkCode;
    
    -- Update score change
    UPDATE Assessment.ComplianceScoreHistory
    SET ScoreChange = ComplianceScore - ISNULL(PreviousScore, ComplianceScore)
    WHERE CAST(AssessmentDate AS DATE) = CAST(@AssessmentDate AS DATE);
    
    -- Return summary
    SELECT 
        FrameworkCode,
        TotalControls,
        PassedControls,
        FailedControls,
        ComplianceScore,
        ComplianceGrade,
        CASE WHEN ScoreChange > 0 THEN '+' ELSE '' END + CAST(ISNULL(ScoreChange, 0) AS VARCHAR) AS Trend
    FROM Assessment.ComplianceScoreHistory
    WHERE CAST(AssessmentDate AS DATE) = CAST(@AssessmentDate AS DATE)
    ORDER BY FrameworkCode;
END

GO

-- [Assessment].[usp_RunComplianceAssessment_AllServers]

-- 1. First, create a procedure to collect from all servers
CREATE   PROCEDURE Assessment.usp_RunComplianceAssessment_AllServers
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @AssessmentDate DATETIME2 = SYSDATETIME();
    DECLARE @ServerName VARCHAR(128);
    DECLARE @SQL NVARCHAR(MAX);
    DECLARE @ErrorMsg NVARCHAR(MAX);
    
    -- Log start
    PRINT 'Starting Compliance Assessment for All Servers at ' + CONVERT(VARCHAR, @AssessmentDate, 120);
    
    -- Clear today's results
    DELETE FROM Assessment.ComplianceControlResults 
    WHERE CAST(AssessmentDate AS DATE) = CAST(@AssessmentDate AS DATE);
    
    DELETE FROM Assessment.ComplianceScoreHistory 
    WHERE CAST(AssessmentDate AS DATE) = CAST(@AssessmentDate AS DATE);
    
    -- Create temp table for results
    IF OBJECT_ID('tempdb..#ServerResults') IS NOT NULL DROP TABLE #ServerResults;
    CREATE TABLE #ServerResults (
        ServerName VARCHAR(128),
        FrameworkCode VARCHAR(20),
        ControlCode VARCHAR(50),
        ControlStatus VARCHAR(20),
        Finding NVARCHAR(MAX),
        Evidence NVARCHAR(MAX)
    );
    
    -- Cursor through all linked servers
    DECLARE server_cursor CURSOR FOR
    SELECT name FROM sys.servers 
    WHERE is_linked = 1 AND name NOT LIKE '%\%'
    ORDER BY name;
    
    OPEN server_cursor;
    FETCH NEXT FROM server_cursor INTO @ServerName;
    
    WHILE @@FETCH_STATUS = 0
    BEGIN
        BEGIN TRY
            PRINT 'Assessing: ' + @ServerName;
            
            -- STIG-V1: Windows Authentication Mode
            SET @SQL = N'
            SELECT @srv, ''STIG'', ''STIG-V1'',
                CASE WHEN SERVERPROPERTY(''IsIntegratedSecurityOnly'') = 1 THEN ''PASS'' ELSE ''FAIL'' END,
                CASE WHEN SERVERPROPERTY(''IsIntegratedSecurityOnly'') = 1 
                    THEN ''Windows Authentication Only mode is enabled''
                    ELSE ''Mixed Mode authentication is enabled'' END,
                ''IsIntegratedSecurityOnly = '' + CAST(ISNULL(SERVERPROPERTY(''IsIntegratedSecurityOnly''), 0) AS VARCHAR)';
            
            INSERT INTO #ServerResults
            EXEC sp_executesql @SQL, N'@srv VARCHAR(128)', @srv = @ServerName;
            
            -- STIG-V2: SA Account
            SET @SQL = N'
            SELECT ''' + @ServerName + ''', ''STIG'', ''STIG-V2'',
                CASE WHEN is_disabled = 1 THEN ''PASS'' ELSE ''FAIL'' END,
                CASE WHEN is_disabled = 1 THEN ''SA account is disabled'' ELSE ''SA account is ENABLED'' END,
                ''SA is_disabled = '' + CAST(is_disabled AS VARCHAR)
            FROM [' + @ServerName + '].master.sys.server_principals WHERE name = ''sa''';
            
            BEGIN TRY
                INSERT INTO #ServerResults EXEC sp_executesql @SQL;
            END TRY
            BEGIN CATCH
                INSERT INTO #ServerResults VALUES (@ServerName, 'STIG', 'STIG-V2', 'ERROR', ERROR_MESSAGE(), 'Query failed');
            END CATCH
            
            -- STIG-V7: xp_cmdshell
            SET @SQL = N'
            SELECT ''' + @ServerName + ''', ''STIG'', ''STIG-V7'',
                CASE WHEN CAST(value_in_use AS INT) = 0 THEN ''PASS'' ELSE ''FAIL'' END,
                CASE WHEN CAST(value_in_use AS INT) = 0 THEN ''xp_cmdshell is disabled'' ELSE ''xp_cmdshell is ENABLED'' END,
                ''xp_cmdshell = '' + CAST(value_in_use AS VARCHAR)
            FROM [' + @ServerName + '].master.sys.configurations WHERE name = ''xp_cmdshell''';
            
            BEGIN TRY
                INSERT INTO #ServerResults EXEC sp_executesql @SQL;
            END TRY
            BEGIN CATCH
                INSERT INTO #ServerResults VALUES (@ServerName, 'STIG', 'STIG-V7', 'ERROR', ERROR_MESSAGE(), 'Query failed');
            END CATCH
            
            -- STIG-V6: CLR
            SET @SQL = N'
            SELECT ''' + @ServerName + ''', ''STIG'', ''STIG-V6'',
                CASE WHEN CAST(value_in_use AS INT) = 0 THEN ''PASS'' ELSE ''FAIL'' END,
                CASE WHEN CAST(value_in_use AS INT) = 0 THEN ''CLR is disabled'' ELSE ''CLR is enabled'' END,
                ''clr enabled = '' + CAST(value_in_use AS VARCHAR)
            FROM [' + @ServerName + '].master.sys.configurations WHERE name = ''clr enabled''';
            
            BEGIN TRY
                INSERT INTO #ServerResults EXEC sp_executesql @SQL;
            END TRY
            BEGIN CATCH
                INSERT INTO #ServerResults VALUES (@ServerName, 'STIG', 'STIG-V6', 'ERROR', ERROR_MESSAGE(), 'Query failed');
            END CATCH
            
            -- STIG-V8: Remote Access
            SET @SQL = N'
            SELECT ''' + @ServerName + ''', ''STIG'', ''STIG-V8'',
                CASE WHEN CAST(value_in_use AS INT) = 0 THEN ''PASS'' ELSE ''FAIL'' END,
                CASE WHEN CAST(value_in_use AS INT) = 0 THEN ''Remote access is disabled'' ELSE ''Remote access is enabled'' END,
                ''remote access = '' + CAST(value_in_use AS VARCHAR)
            FROM [' + @ServerName + '].master.sys.configurations WHERE name = ''remote access''';
            
            BEGIN TRY
                INSERT INTO #ServerResults EXEC sp_executesql @SQL;
            END TRY
            BEGIN CATCH
                INSERT INTO #ServerResults VALUES (@ServerName, 'STIG', 'STIG-V8', 'ERROR', ERROR_MESSAGE(), 'Query failed');
            END CATCH
            
            -- STIG-V9: Cross DB Ownership
            SET @SQL = N'
            SELECT ''' + @ServerName + ''', ''STIG'', ''STIG-V9'',
                CASE WHEN CAST(value_in_use AS INT) = 0 THEN ''PASS'' ELSE ''FAIL'' END,
                CASE WHEN CAST(value_in_use AS INT) = 0 THEN ''Cross DB ownership chaining disabled'' ELSE ''Cross DB ownership chaining enabled'' END,
                ''cross db ownership chaining = '' + CAST(value_in_use AS VARCHAR)
            FROM [' + @ServerName + '].master.sys.configurations WHERE name = ''cross db ownership chaining''';
            
            BEGIN TRY
                INSERT INTO #ServerResults EXEC sp_executesql @SQL;
            END TRY
            BEGIN CATCH
                INSERT INTO #ServerResults VALUES (@ServerName, 'STIG', 'STIG-V9', 'ERROR', ERROR_MESSAGE(), 'Query failed');
            END CATCH
            
            -- DISA-AU1: Server Audits
            SET @SQL = N'
            SELECT ''' + @ServerName + ''', ''DISA'', ''DISA-AU1'',
                CASE WHEN COUNT(*) > 0 THEN ''PASS'' ELSE ''FAIL'' END,
                CASE WHEN COUNT(*) > 0 THEN CAST(COUNT(*) AS VARCHAR) + '' server audit(s) configured'' ELSE ''No server audits'' END,
                ''Audits: '' + CAST(COUNT(*) AS VARCHAR)
            FROM [' + @ServerName + '].master.sys.server_audits WHERE is_state_enabled = 1';
            
            BEGIN TRY
                INSERT INTO #ServerResults EXEC sp_executesql @SQL;
            END TRY
            BEGIN CATCH
                INSERT INTO #ServerResults VALUES (@ServerName, 'DISA', 'DISA-AU1', 'ERROR', ERROR_MESSAGE(), 'Query failed');
            END CATCH
            
            -- NIST-ID.AM: Asset tracked
            INSERT INTO #ServerResults VALUES (@ServerName, 'NIST', 'NIST-ID.AM', 'PASS', 'Server is tracked in inventory', 'In linked servers');
            
            -- CJIS-5.5: Access Control
            INSERT INTO #ServerResults VALUES (@ServerName, 'CJIS', 'CJIS-5.5', 'PASS', 'Server has access controls via AD', 'Windows Authentication');
            
            PRINT '  SUCCESS: ' + @ServerName;
            
        END TRY
        BEGIN CATCH
            SET @ErrorMsg = ERROR_MESSAGE();
            PRINT '  ERROR: ' + @ServerName + ' - ' + @ErrorMsg;
            INSERT INTO #ServerResults VALUES (@ServerName, 'ERROR', 'CONNECTION', 'ERROR', @ErrorMsg, 'Server unreachable');
        END CATCH
        
        FETCH NEXT FROM server_cursor INTO @ServerName;
    END
    
    CLOSE server_cursor;
    DEALLOCATE server_cursor;
    
    -- Insert results into permanent table
    INSERT INTO Assessment.ComplianceControlResults (AssessmentDate, FrameworkCode, ControlCode, ServerName, ControlStatus, Finding, Evidence)
    SELECT @AssessmentDate, FrameworkCode, ControlCode, ServerName, ControlStatus, Finding, Evidence
    FROM #ServerResults
    WHERE ControlStatus != 'ERROR';
    
    -- Calculate scores by framework (aggregated across all servers)
    INSERT INTO Assessment.ComplianceScoreHistory (
        AssessmentDate, FrameworkCode, TotalControls, PassedControls, 
        FailedControls, NotApplicable, ComplianceScore, ComplianceGrade, PreviousScore, ScoreChange
    )
    SELECT 
        @AssessmentDate,
        FrameworkCode,
        COUNT(*) AS TotalControls,
        SUM(CASE WHEN ControlStatus = 'PASS' THEN 1 ELSE 0 END) AS PassedControls,
        SUM(CASE WHEN ControlStatus = 'FAIL' THEN 1 ELSE 0 END) AS FailedControls,
        SUM(CASE WHEN ControlStatus = 'N/A' THEN 1 ELSE 0 END) AS NotApplicable,
        CAST(SUM(CASE WHEN ControlStatus = 'PASS' THEN 1 ELSE 0 END) * 100.0 / NULLIF(COUNT(*), 0) AS DECIMAL(5,2)),
        CASE 
            WHEN SUM(CASE WHEN ControlStatus = 'PASS' THEN 1 ELSE 0 END) * 100.0 / NULLIF(COUNT(*), 0) >= 95 THEN 'A'
            WHEN SUM(CASE WHEN ControlStatus = 'PASS' THEN 1 ELSE 0 END) * 100.0 / NULLIF(COUNT(*), 0) >= 85 THEN 'B'
            WHEN SUM(CASE WHEN ControlStatus = 'PASS' THEN 1 ELSE 0 END) * 100.0 / NULLIF(COUNT(*), 0) >= 75 THEN 'C'
            WHEN SUM(CASE WHEN ControlStatus = 'PASS' THEN 1 ELSE 0 END) * 100.0 / NULLIF(COUNT(*), 0) >= 65 THEN 'D'
            ELSE 'F'
        END,
        NULL,
        0
    FROM Assessment.ComplianceControlResults
    WHERE CAST(AssessmentDate AS DATE) = CAST(@AssessmentDate AS DATE)
      AND FrameworkCode != 'ERROR'
    GROUP BY FrameworkCode;
    
    -- Return summary
    SELECT 
        FrameworkCode,
        TotalControls,
        PassedControls,
        FailedControls,
        ComplianceScore,
        ComplianceGrade
    FROM Assessment.ComplianceScoreHistory
    WHERE CAST(AssessmentDate AS DATE) = CAST(@AssessmentDate AS DATE)
    ORDER BY FrameworkCode;
    
    PRINT 'Compliance Assessment Complete at ' + CONVERT(VARCHAR, GETDATE(), 120);
END

GO

-- [Assessment].[usp_SendAssessmentNotification]

CREATE PROCEDURE Assessment.usp_SendAssessmentNotification
    @AssessmentDate DATETIME2 = NULL,
    @NotificationReason NVARCHAR(500) = 'Scheduled Assessment',
    @OnlyOnIssues BIT = 1  -- NEW: Only send if critical/high findings
AS
BEGIN
    SET NOCOUNT ON;
    
    IF @AssessmentDate IS NULL
        SET @AssessmentDate = SYSDATETIME();
    
    -- Get configuration
    DECLARE @RecipientEmail NVARCHAR(500);
    DECLARE @MailProfile NVARCHAR(128);
    
    SELECT @RecipientEmail = ConfigValue FROM Assessment.Configuration WHERE ConfigKey = 'RecipientEmail';
    SELECT @MailProfile = NULLIF(ConfigValue, '') FROM Assessment.Configuration WHERE ConfigKey = 'MailProfile';
    
    SET @RecipientEmail = ISNULL(@RecipientEmail, 'SQLServerAdminNotification@YOURORG.gov');
    
    -- Get latest assessment data
    DECLARE @MaturityScore DECIMAL(5,2);
    DECLARE @CriticalCount INT;
    DECLARE @HighCount INT;
    DECLARE @MediumCount INT;
    DECLARE @LowCount INT;
    DECLARE @TotalFindings INT;
    
    SELECT TOP 1
        @MaturityScore = MaturityScore,
        @TotalFindings = TotalFindings,
        @CriticalCount = CriticalFindings,
        @HighCount = HighFindings,
        @MediumCount = MediumFindings,
        @LowCount = LowFindings
    FROM Assessment.AssessmentHistory
    WHERE CAST(AssessmentDate AS DATE) = CAST(@AssessmentDate AS DATE)
    ORDER BY AssessmentDate DESC;
    
    -- =====================================================
    -- SMART EMAIL LOGIC: Only send if critical/high findings
    -- =====================================================
    IF @OnlyOnIssues = 1 AND ISNULL(@CriticalCount, 0) = 0 AND ISNULL(@HighCount, 0) = 0
    BEGIN
        PRINT 'DBAOps | JB: Assessment score ' + CAST(ISNULL(@MaturityScore, 0) AS VARCHAR(10)) + 
            '%. No critical/high findings. Email not sent.';
        
        -- Still update notification status
        UPDATE Assessment.AssessmentHistory
        SET NotificationSent = 0
        WHERE CAST(AssessmentDate AS DATE) = CAST(@AssessmentDate AS DATE);
        
        RETURN;
    END;
    
    -- Build email subject
    DECLARE @Subject NVARCHAR(500);
    SET @Subject = CASE 
        WHEN ISNULL(@CriticalCount, 0) > 0 THEN '[CRITICAL] '
        WHEN ISNULL(@HighCount, 0) > 0 THEN '[WARNING] '
        ELSE '[OK] '
    END + 'DBAOps Assessment - ' + @@SERVERNAME + ' - Score: ' + CAST(ISNULL(@MaturityScore, 0) AS VARCHAR(10)) + '%';
    
    -- Build simple HTML body
    DECLARE @Body NVARCHAR(MAX);
    SET @Body = N'<html><body style="font-family:Arial;">
    <h2>DBAOps Architecture Assessment Report</h2>
    <p><strong>Server:</strong> ' + @@SERVERNAME + N'</p>
    <p><strong>Score:</strong> ' + CAST(ISNULL(@MaturityScore, 0) AS VARCHAR(10)) + N'%</p>
    <p><strong>This report only sent when critical/high findings exist.</strong></p>
    <table border="1" cellpadding="5">
    <tr><td>Critical Findings</td><td style="color:red;font-weight:bold;">' + CAST(ISNULL(@CriticalCount, 0) AS VARCHAR(10)) + N'</td></tr>
    <tr><td>High Findings</td><td style="color:orange;font-weight:bold;">' + CAST(ISNULL(@HighCount, 0) AS VARCHAR(10)) + N'</td></tr>
    <tr><td>Medium Findings</td><td>' + CAST(ISNULL(@MediumCount, 0) AS VARCHAR(10)) + N'</td></tr>
    <tr><td>Low Findings</td><td>' + CAST(ISNULL(@LowCount, 0) AS VARCHAR(10)) + N'</td></tr>
    </table>
    <p style="color:gray;">DBAOps Enterprise™ Framework | John Bosco RHOYLLE INNOVATION EMPIRE™</p>
    </body></html>';
    
    -- Send email
    BEGIN TRY
        EXEC msdb.dbo.sp_send_dbmail
            @profile_name = @MailProfile,
            @recipients = @RecipientEmail,
            @subject = @Subject,
            @body = @Body,
            @body_format = 'HTML';
        
        UPDATE Assessment.AssessmentHistory
        SET NotificationSent = 1,
            NotificationTime = SYSDATETIME()
        WHERE CAST(AssessmentDate AS DATE) = CAST(@AssessmentDate AS DATE);
        
        PRINT 'DBAOps | JB: Assessment notification sent - ' + 
            CAST(ISNULL(@CriticalCount, 0) AS VARCHAR) + ' critical, ' +
            CAST(ISNULL(@HighCount, 0) AS VARCHAR) + ' high';
    END TRY
    BEGIN CATCH
        PRINT 'Failed to send email: ' + ERROR_MESSAGE();
    END CATCH
END;

GO

-- [audit].[CheckDeadlockThresholds]
CREATE OR ALTER PROCEDURE audit.CheckDeadlockThresholds
AS
BEGIN
    SET NOCOUNT ON;

    WITH DeadlockCounts AS
    (
        SELECT
            ServerName,
            COUNT(*) AS DeadlockCount
        FROM audit.DeadlockEvents
        WHERE EventTime >= DATEADD(HOUR, -24, SYSUTCDATETIME())
        GROUP BY ServerName
    )
    SELECT
        dc.ServerName,
        dc.DeadlockCount,
        dt.WarningThreshold,
        dt.CriticalThreshold,
        CASE
            WHEN dc.DeadlockCount >= dt.CriticalThreshold THEN 'CRITICAL'
            WHEN dc.DeadlockCount >= dt.WarningThreshold THEN 'WARNING'
            ELSE 'NORMAL'
        END AS Severity
    INTO #Alerts
    FROM DeadlockCounts dc
    JOIN audit.DeadlockThresholds dt
      ON dc.ServerName = dt.ServerName;

    INSERT INTO audit.DeadlockThresholdLog (ServerName, DeadlockCount, Severity, LoggedAt)
    SELECT ServerName, DeadlockCount, Severity, SYSUTCDATETIME()
    FROM #Alerts
    WHERE Severity IN ('WARNING','CRITICAL');

    DECLARE @Body NVARCHAR(MAX) = '';

    SELECT @Body =
        COALESCE(@Body,'') + 'Server: ' + ServerName + ', Count: ' 
        + CAST(DeadlockCount AS VARCHAR(10)) + ' Severity: ' + Severity + CHAR(13)
    FROM #Alerts
    WHERE Severity = 'CRITICAL';

    IF LEN(@Body) > 0
    BEGIN
        EXEC msdb.dbo.sp_send_dbmail
            @profile_name = N'DBAOpsProfile',
            @recipients = N'SQLServerAdminNotification@YOURORG.gov',
            @subject = N'ðŸš¨ Critical Deadlock Threshold Breach',
            @body = @Body;
    END
END

GO

-- [audit].[DeployDeadlockThresholdJob]

CREATE   PROCEDURE audit.DeployDeadlockThresholdJob
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @jobId UNIQUEIDENTIFIER;
    DECLARE @schedule_id INT;
    DECLARE @step_id INT;

    -- Get Job ID
    SELECT @jobId = job_id 
    FROM msdb.dbo.sysjobs 
    WHERE name = N'Check Deadlock Thresholds';

    IF @jobId IS NOT NULL
    BEGIN
        -- Check for step and get step_id
        SELECT @step_id = step_id
        FROM msdb.dbo.sysjobsteps
        WHERE job_id = @jobId AND step_name = N'Run Threshold Check';

        IF @step_id IS NOT NULL
        BEGIN
            -- âœ… FIXED: Supplying @step_id now
            EXEC msdb.dbo.sp_update_jobstep
                @job_id = @jobId,
                @step_id = @step_id,
                @step_name = N'Run Threshold Check',
                @subsystem = N'TSQL',
                @database_name = N'DBAOpsRepository',
                @command = N'EXEC audit.CheckDeadlockThresholds;';
        END
        ELSE
        BEGIN
            EXEC msdb.dbo.sp_add_jobstep
                @job_id = @jobId,
                @step_name = N'Run Threshold Check',
                @subsystem = N'TSQL',
                @database_name = N'DBAOpsRepository',
                @command = N'EXEC audit.CheckDeadlockThresholds;';
        END

        -- Re-enable job just in case
        EXEC msdb.dbo.sp_update_job 
            @job_id = @jobId,
            @enabled = 1;
    END
    ELSE
    BEGIN
        -- Create new job
        EXEC msdb.dbo.sp_add_job 
            @job_name = N'Check Deadlock Thresholds',
            @enabled = 1,
            @description = N'Evaluates daily deadlock counts vs compliance thresholds',
            @owner_login_name = N'sa',
            @job_id = @jobId OUTPUT;

        -- Add step
        EXEC msdb.dbo.sp_add_jobstep
            @job_id = @jobId,
            @step_name = N'Run Threshold Check',
            @subsystem = N'TSQL',
            @database_name = N'DBAOpsRepository',
            @command = N'EXEC audit.CheckDeadlockThresholds;';
    END

    -- Create or reuse schedule
    SELECT @schedule_id = schedule_id 
    FROM msdb.dbo.sysschedules 
    WHERE name = N'DeadlockThresholdDailyCheck';

    IF @schedule_id IS NULL
    BEGIN
        EXEC msdb.dbo.sp_add_schedule 
            @schedule_name = N'DeadlockThresholdDailyCheck',
            @enabled = 1,
            @freq_type = 4,               -- Daily
            @freq_interval = 1,
            @active_start_time = 235900,
            @schedule_id = @schedule_id OUTPUT;
    END

    -- Attach schedule to job
    IF NOT EXISTS (
        SELECT 1
        FROM msdb.dbo.sysjobschedules 
        WHERE job_id = @jobId AND schedule_id = @schedule_id
    )
    BEGIN
        EXEC msdb.dbo.sp_attach_schedule 
            @job_id = @jobId,
            @schedule_id = @schedule_id;
    END

    -- Attach job to server
    IF NOT EXISTS (
        SELECT 1 FROM msdb.dbo.sysjobservers WHERE job_id = @jobId
    )
    BEGIN
        EXEC msdb.dbo.sp_add_jobserver 
            @job_id = @jobId;
    END
END;

GO

-- [audit].[PullDeadlocksFromLinkedServers]

CREATE   PROCEDURE [audit].[PullDeadlocksFromLinkedServers]
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @ServerName SYSNAME, @SQL NVARCHAR(MAX);
    
    DECLARE cur CURSOR LOCAL FAST_FORWARD FOR
    SELECT name FROM sys.servers WHERE is_linked = 1;
    
    OPEN cur;
    FETCH NEXT FROM cur INTO @ServerName;
    
    WHILE @@FETCH_STATUS = 0
    BEGIN
        SET @SQL = '
        INSERT INTO [DBAOpsRepository].[audit].[DeadlockEvents] (
            DeadlockID, ServerName, EventTime, DeadlockGraph, CollectedAt
        )
        SELECT 
            NEWID(),
            ''' + @ServerName + ''',
            DATEADD(HOUR, DATEDIFF(HOUR, GETUTCDATE(), SYSDATETIMEOFFSET()),
                 CAST(XEvent.value(''(event/@timestamp)[1]'', ''datetime2'') AS DATETIME2)),
            XEvent.query(''.''),
            SYSDATETIME()
        FROM (
            SELECT CAST(event_data AS XML) AS XEvent
            FROM OPENQUERY(' + QUOTENAME(@ServerName) + ', 
                 ''
                SELECT CAST(event_data AS XML) AS event_data
                FROM sys.dm_xe_session_targets t
                JOIN sys.dm_xe_sessions s ON t.event_session_address = s.address
                WHERE s.name = ''''CaptureDeadlocks''''
                AND t.target_name = ''''ring_buffer''''
                '')
        ) AS Deadlocks
        WHERE XEvent.exist(''/event[@name="xml_deadlock_report"]'') = 1;
        ';
        
        BEGIN TRY
            EXEC sp_executesql @SQL;
        END TRY
        BEGIN CATCH
            PRINT 'Error from ' + @ServerName + ': ' + ERROR_MESSAGE();
        END CATCH;
        
        FETCH NEXT FROM cur INTO @ServerName;
    END
    
    CLOSE cur;
    DEALLOCATE cur;
END

GO

-- [audit].[PurgeOldDeadlocks]
CREATE OR ALTER PROCEDURE audit.PurgeOldDeadlocks
    @DaysToKeep INT = 90
AS
BEGIN
    DELETE FROM audit.DeadlockEvents
    WHERE EventTime < DATEADD(DAY, -@DaysToKeep, SYSUTCDATETIME());

    DELETE FROM audit.DeadlockThresholdLog
    WHERE LoggedAt < DATEADD(DAY, -@DaysToKeep, SYSUTCDATETIME());
END

GO

-- [audit].[usp_Audit_CleanUp]

CREATE PROCEDURE audit.usp_Audit_CleanUp
    @RetentionDays INT = 90
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @CutoffDate DATETIME = DATEADD(DAY, -@RetentionDays, GETDATE());
    DECLARE @DeletedRows INT;
    
    PRINT 'Cleaning audit data older than ' + CAST(@RetentionDays AS VARCHAR) + ' days...';
    
    IF OBJECT_ID('audit.PermissionChanges', 'U') IS NOT NULL
    BEGIN
        DELETE FROM audit.PermissionChanges WHERE EventTime < @CutoffDate;
        SET @DeletedRows = @@ROWCOUNT;
        IF @DeletedRows > 0 PRINT '  PermissionChanges: ' + CAST(@DeletedRows AS VARCHAR) + ' rows deleted';
    END
    
    IF OBJECT_ID('audit.LoginFailures', 'U') IS NOT NULL
    BEGIN
        DELETE FROM audit.LoginFailures WHERE EventTime < @CutoffDate;
        SET @DeletedRows = @@ROWCOUNT;
        IF @DeletedRows > 0 PRINT '  LoginFailures: ' + CAST(@DeletedRows AS VARCHAR) + ' rows deleted';
    END
    
    IF OBJECT_ID('audit.ObjectChanges', 'U') IS NOT NULL
    BEGIN
        DELETE FROM audit.ObjectChanges WHERE EventTime < @CutoffDate;
        SET @DeletedRows = @@ROWCOUNT;
        IF @DeletedRows > 0 PRINT '  ObjectChanges: ' + CAST(@DeletedRows AS VARCHAR) + ' rows deleted';
    END
    
    IF OBJECT_ID('audit.AuditLog', 'U') IS NOT NULL
    BEGIN
        DELETE FROM audit.AuditLog WHERE EventTime < DATEADD(DAY, -30, GETDATE());
        SET @DeletedRows = @@ROWCOUNT;
        IF @DeletedRows > 0 PRINT '  AuditLog: ' + CAST(@DeletedRows AS VARCHAR) + ' rows deleted';
    END
    
    PRINT 'Cleanup complete';
END

GO

-- [audit].[usp_Audit_ServerSummary]
CREATE OR ALTER PROCEDURE audit.usp_Audit_ServerSummary
AS
BEGIN
    SET NOCOUNT ON;

    INSERT INTO audit.AuditSummary
    (
        TotalLoginsFailed,
        TotalLoginsSucceeded,
        TotalPasswordChanges,
        TotalServerRestarts,
        TotalAgentFailures,
        TotalSeverityErrors,
        TotalDeadlocks
    )
    SELECT
        (SELECT COUNT(*) FROM audit.AuditLog WHERE ActionType='LoginFailed'    AND EventTime >= DATEADD(DAY,-1,GETDATE())),
        (SELECT COUNT(*) FROM audit.AuditLog WHERE ActionType='LoginSuccess'   AND EventTime >= DATEADD(DAY,-1,GETDATE())),
        (SELECT COUNT(*) FROM audit.AuditLog WHERE ActionType='PasswordChange' AND EventTime >= DATEADD(DAY,-1,GETDATE())),
        (SELECT COUNT(*) FROM audit.AuditLog WHERE ActionType='ServerRestart'  AND EventTime >= DATEADD(DAY,-1,GETDATE())),
        (SELECT COUNT(*) FROM audit.AuditLog WHERE ActionType='AgentFailure'   AND EventTime >= DATEADD(DAY,-1,GETDATE())),
        (SELECT COUNT(*) FROM audit.AuditLog WHERE ActionType='ErrorEvent'     AND EventTime >= DATEADD(DAY,-1,GETDATE())),
        (SELECT COUNT(*) FROM audit.AuditLog WHERE ActionType='DeadlockEvent'  AND EventTime >= DATEADD(DAY,-1,GETDATE()))
    ;
END

GO

-- [audit].[usp_AuditLoginActivity]

CREATE PROCEDURE audit.usp_AuditLoginActivity
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @Inserted INT = 0;
    
    -- Insert into security.LoginAudit
    -- Columns: AuditID, ServerName, LoginName, EventType, EventTime, ClientHost,
    --          ApplicationName, ErrorNumber, ErrorMessage, CollectedDate
    
    INSERT INTO security.LoginAudit (
        ServerName, LoginName, EventType, EventTime, ClientHost, 
        ApplicationName, ErrorNumber, ErrorMessage, CollectedDate
    )
    SELECT 
        @@SERVERNAME AS ServerName,
        sp.name AS LoginName,
        sp.type_desc AS EventType,
        sp.create_date AS EventTime,
        NULL AS ClientHost,
        NULL AS ApplicationName,
        0 AS ErrorNumber,
        NULL AS ErrorMessage,
        SYSDATETIME() AS CollectedDate
    FROM sys.server_principals sp
    WHERE sp.type IN ('S', 'U', 'G')
      AND sp.name NOT LIKE '##%'
      AND sp.name NOT IN ('sa', 'public')
      AND NOT EXISTS (
          SELECT 1 FROM security.LoginAudit la
          WHERE la.ServerName = @@SERVERNAME
            AND la.LoginName = sp.name
            AND la.CollectedDate > DATEADD(HOUR, -1, SYSDATETIME())
      );
    
    SET @Inserted = @@ROWCOUNT;
    
    -- Log the operation
    INSERT INTO log.AutomationLog (ModuleName, RunStatus, Message, ServerName, RowsAffected, LogType, LogTime)
    VALUES ('AuditLoginActivity', 'Success', 'Audited ' + CAST(@Inserted AS VARCHAR(10)) + ' logins',
            @@SERVERNAME, @Inserted, 'INFO', SYSDATETIME());
    
    PRINT 'Login audit completed: ' + CAST(@Inserted AS VARCHAR(10)) + ' logins';
    
    SELECT @Inserted AS LoginsAudited;
END;

GO

-- [audit].[usp_Collect_LoginFailures]

CREATE PROCEDURE audit.usp_Collect_LoginFailures
AS
BEGIN
    SET NOCOUNT ON;
    
    BEGIN TRY
        DECLARE @TraceFile NVARCHAR(500);
        DECLARE @RowsInserted INT;
        
        SELECT @TraceFile = path FROM sys.traces WHERE is_default = 1;
        
        IF @TraceFile IS NULL
        BEGIN
            PRINT 'Default trace not enabled';
            RETURN;
        END
        
        DECLARE @LastCollection DATETIME;
        SELECT @LastCollection = ISNULL(MAX(EventTime), DATEADD(DAY, -1, GETDATE())) 
        FROM audit.LoginFailures;
        
        INSERT INTO audit.LoginFailures (EventTime, LoginName, ClientHost, ErrorMessage)
        SELECT 
            StartTime,
            ISNULL(LoginName, 'Unknown'),
            ISNULL(HostName, 'Unknown'),
            ISNULL(CAST(TextData AS NVARCHAR(MAX)), '')
        FROM fn_trace_gettable(@TraceFile, DEFAULT)
        WHERE EventClass = 20
        AND StartTime > @LastCollection;
        
        SET @RowsInserted = @@ROWCOUNT;
        PRINT 'Collected ' + CAST(@RowsInserted AS VARCHAR) + ' login failure events';
    END TRY
    BEGIN CATCH
        PRINT 'Error collecting login failures: ' + ERROR_MESSAGE();
    END CATCH
END

GO

-- [audit].[usp_Collect_ObjectChanges]

CREATE PROCEDURE audit.usp_Collect_ObjectChanges
AS
BEGIN
    SET NOCOUNT ON;
    
    BEGIN TRY
        DECLARE @TraceFile NVARCHAR(500);
        SELECT @TraceFile = path FROM sys.traces WHERE is_default = 1;
        
        IF @TraceFile IS NULL
        BEGIN
            PRINT 'Default trace not enabled';
            RETURN;
        END
        
        DECLARE @LastCollection DATETIME;
        SELECT @LastCollection = ISNULL(MAX(EventTime), DATEADD(DAY, -1, GETDATE())) FROM audit.ObjectChanges;
        
        INSERT INTO audit.ObjectChanges (EventTime, DatabaseName, ObjectName, ObjectType, EventType, ChangedBy, CommandText)
        SELECT 
            StartTime,
            DatabaseName,
            ObjectName,
            CAST(ObjectType AS NVARCHAR(50)),
            te.name,
            LoginName,
            CAST(TextData AS NVARCHAR(MAX))
        FROM fn_trace_gettable(@TraceFile, DEFAULT) t
        JOIN sys.trace_events te ON t.EventClass = te.trace_event_id
        WHERE t.StartTime > @LastCollection
        AND te.name IN ('Object:Created', 'Object:Deleted', 'Object:Altered')
        AND ISNULL(t.ObjectName, '') NOT LIKE '_WA_Sys_%'
        AND ISNULL(t.ObjectName, '') NOT LIKE '#%'
        AND ISNULL(t.DatabaseName, '') NOT IN ('tempdb')
        AND t.LoginName IS NOT NULL;
        
        PRINT 'Collected ' + CAST(@@ROWCOUNT AS VARCHAR) + ' object change events';
    END TRY
    BEGIN CATCH
        PRINT 'Error: ' + ERROR_MESSAGE();
    END CATCH
END

GO

-- [audit].[usp_Collect_PermissionChanges]

CREATE PROCEDURE audit.usp_Collect_PermissionChanges
AS
BEGIN
    SET NOCOUNT ON;
    
    BEGIN TRY
        DECLARE @TraceFile NVARCHAR(500);
        SELECT @TraceFile = path FROM sys.traces WHERE is_default = 1;
        
        IF @TraceFile IS NULL
        BEGIN
            PRINT 'Default trace not enabled';
            RETURN;
        END
        
        DECLARE @LastCollection DATETIME;
        SELECT @LastCollection = ISNULL(MAX(EventTime), DATEADD(DAY, -1, GETDATE())) FROM audit.PermissionChanges;
        
        INSERT INTO audit.PermissionChanges (EventTime, DatabaseName, ObjectName, ChangedBy, ChangeType, CommandText)
        SELECT 
            StartTime,
            DatabaseName,
            ObjectName,
            LoginName,
            te.name,
            CAST(TextData AS NVARCHAR(MAX))
        FROM fn_trace_gettable(@TraceFile, DEFAULT) t
        JOIN sys.trace_events te ON t.EventClass = te.trace_event_id
        WHERE t.StartTime > @LastCollection
        AND te.name IN ('Audit Add DB User Event', 'Audit Add Login to Server Role Event', 
                        'Audit Add Member to DB Role Event', 'Audit Add Role Event',
                        'Audit Database Permission Event', 'Audit Schema Object Access Event',
                        'Audit Server Principal Management Event')
        AND t.LoginName IS NOT NULL;
        
        PRINT 'Collected ' + CAST(@@ROWCOUNT AS VARCHAR) + ' permission change events';
    END TRY
    BEGIN CATCH
        PRINT 'Error: ' + ERROR_MESSAGE();
    END CATCH
END

GO

-- [audit].[usp_SendAlert]

-- ============================================================================
-- STEP 2: Fix the usp_SendAlert stored procedure
-- ============================================================================
CREATE PROCEDURE audit.usp_SendAlert
    @AlertType      NVARCHAR(100),
    @ServerName     SYSNAME,
    @Details        NVARCHAR(MAX)
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @Subject    NVARCHAR(255);
    DECLARE @Body       NVARCHAR(MAX);
    DECLARE @Recipients NVARCHAR(500) = 'DBAOps@YOURORG.gov; SQLServerAdminNotification@YOURORG.gov';
    
    -- Sanitize inputs to prevent SQL injection and syntax errors
    SET @Details    = REPLACE(ISNULL(@Details, ''), '''', '''''');
    SET @ServerName = REPLACE(ISNULL(@ServerName, 'UNKNOWN'), '''', '''''');
    SET @AlertType  = REPLACE(ISNULL(@AlertType, 'General'), '''', '''''');
    
    -- Build alert subject and body
    SET @Subject = CONCAT('SQL Alert [', @AlertType, '] on ', @ServerName);
    SET @Body = CONCAT(
        'Alert Type: ', @AlertType, CHAR(13), CHAR(10),
        'Server: ', @ServerName, CHAR(13), CHAR(10),
        'Time: ', CONVERT(VARCHAR(30), SYSDATETIME(), 121), CHAR(13), CHAR(10),
        'Details: ', @Details
    );
    
    -- Log the alert (using parameterized query to avoid injection)
    IF OBJECT_ID('audit.AlertLog', 'U') IS NOT NULL
    BEGIN
        INSERT INTO audit.AlertLog (AlertType, ServerName, Details, AlertTime)
        VALUES (@AlertType, @ServerName, @Details, SYSDATETIME());
    END
    
    -- Send email notification
    BEGIN TRY
        EXEC msdb.dbo.sp_send_dbmail
            @profile_name = 'DBAOpsProfile',
            @recipients   = @Recipients,
            @subject      = @Subject,
            @body         = @Body,
            @body_format  = 'TEXT';
    END TRY
    BEGIN CATCH
        -- Log email failure but don't fail the procedure
        PRINT 'Email send failed: ' + ERROR_MESSAGE();
    END CATCH
END

GO

-- [audit].[usp_SendAuditEmail]

CREATE PROCEDURE audit.usp_SendAuditEmail
    @OnlyOnActivity BIT = 1  -- NEW: Only send if activity detected
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @ProfileName NVARCHAR(128);
    DECLARE @Subject NVARCHAR(255);
    DECLARE @Body NVARCHAR(MAX);
    DECLARE @LoginFailures INT = 0;
    DECLARE @ObjectChanges INT = 0;
    
    -- Get counts
    IF OBJECT_ID('audit.LoginFailures', 'U') IS NOT NULL
        SELECT @LoginFailures = COUNT(*) FROM audit.LoginFailures 
        WHERE EventTime >= DATEADD(HOUR, -24, GETDATE());
    
    IF OBJECT_ID('audit.ObjectChanges', 'U') IS NOT NULL
        SELECT @ObjectChanges = COUNT(*) FROM audit.ObjectChanges 
        WHERE EventTime >= DATEADD(HOUR, -24, GETDATE());
    
    -- =====================================================
    -- SMART EMAIL LOGIC: Only send if activity detected
    -- =====================================================
    IF @OnlyOnActivity = 1 AND @LoginFailures = 0 AND @ObjectChanges = 0
    BEGIN
        PRINT 'DBAOps | JB: No audit activity in 24h. Email not sent.';
        RETURN;
    END;
    
    -- Only send if significant activity
    IF @OnlyOnActivity = 1 AND @LoginFailures < 10 AND @ObjectChanges < 5
    BEGIN
        PRINT 'DBAOps | JB: Minimal audit activity (' + 
            CAST(@LoginFailures AS VARCHAR) + ' login failures, ' +
            CAST(@ObjectChanges AS VARCHAR) + ' changes). Email not sent.';
        RETURN;
    END;
    
    SET @Subject = 'DBAOps Audit Summary - ' + CONVERT(VARCHAR, GETDATE(), 107) +
        CASE WHEN @LoginFailures > 50 THEN ' - HIGH LOGIN FAILURES' ELSE '' END;
    SET @Body = 'Login Failures (24h): ' + CAST(@LoginFailures AS VARCHAR) + CHAR(13) + CHAR(10) +
                'Object Changes (24h): ' + CAST(@ObjectChanges AS VARCHAR) + CHAR(13) + CHAR(10) +
                CHAR(13) + CHAR(10) +
                'This report only sent when significant activity detected.';
    
    SELECT TOP 1 @ProfileName = p.name FROM msdb.dbo.sysmail_profile p;
    
    IF @ProfileName IS NOT NULL
    BEGIN
        BEGIN TRY
            EXEC msdb.dbo.sp_send_dbmail
                @profile_name = @ProfileName,
                @recipients = N'DBAOps@YOURORG.gov',
                @subject = @Subject,
                @body = @Body;
            PRINT 'DBAOps | JB: Audit email sent - ' + CAST(@LoginFailures AS VARCHAR) + ' failures, ' + CAST(@ObjectChanges AS VARCHAR) + ' changes';
        END TRY
        BEGIN CATCH
            PRINT 'Email error: ' + ERROR_MESSAGE();
        END CATCH
    END
END;

GO

-- [audit].[usp_SendDailyAuditReport]
CREATE OR ALTER PROCEDURE audit.usp_SendDailyAuditReport
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @Body NVARCHAR(MAX);

    SELECT @Body = (
        SELECT 
            CONCAT(
                '<tr>',
                '<td>', CONVERT(varchar(19), EventTime, 120), '</td>',
                '<td>', LoginName, '</td>',
                '<td>', HostName, '</td>',
                '<td>', ActionType, '</td>',
                '<td>', ISNULL(ObjectName,''), '</td>',
                '<td>', ISNULL(ObjectType,''), '</td>',
                '<td>', TSQLCommand, '</td>',
                '</tr>'
            )
        FROM audit.AuditLog
        WHERE EventTime >= DATEADD(day, -1, GETDATE())
        ORDER BY EventTime DESC
        FOR XML PATH(''), TYPE
    ).value('.', 'NVARCHAR(MAX)');

    IF (@Body IS NULL)
    BEGIN
        PRINT 'No audit events found for last 24 hours.';
        RETURN;
    END

    SET @Body = 
        '<h2>DBAOps Daily Audit Report</h2>' +
        '<p>Events from last 24 hours.</p>' +
        '<table border="1" cellpadding="5" cellspacing="0">
            <tr>
                <th>EventTime</th>
                <th>LoginName</th>
                <th>HostName</th>
                <th>ActionType</th>
                <th>ObjectName</th>
                <th>ObjectType</th>
                <th>TSQLCommand</th>
            </tr>' +
        @Body + '</table>';

    EXEC msdb.dbo.sp_send_dbmail
        @profile_name = 'DBAOpsProfile',
        @recipients   = 'SQLServerAdminNotification@YOURORG.gov',
        @subject      = 'DBAOps Daily Audit Report',
        @body         = @Body,
        @body_format  = 'HTML';
END;

GO

-- [audit].[usp_ValidateAuditIntegrity]

CREATE PROCEDURE audit.usp_ValidateAuditIntegrity
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @InvalidRecords INT = 0;
    DECLARE @TotalRecords BIGINT = 0;
    
    -- Check if audit.AuditLog exists
    IF OBJECT_ID('audit.AuditLog', 'U') IS NOT NULL
    BEGIN
        SELECT @TotalRecords = COUNT(*) FROM audit.AuditLog;
        
        -- Get actual column names from the table
        DECLARE @Columns TABLE (ColumnName NVARCHAR(128));
        INSERT INTO @Columns
        SELECT COLUMN_NAME 
        FROM INFORMATION_SCHEMA.COLUMNS 
        WHERE TABLE_SCHEMA = 'audit' 
          AND TABLE_NAME = 'AuditLog';
        
        -- Basic validation - check for NULL in key columns (adapt to actual schema)
        -- Your audit.AuditLog has: AuditID, Timestamp, Category, Message, Severity, etc.
        SELECT @InvalidRecords = COUNT(*)
        FROM audit.AuditLog
        WHERE AuditID IS NULL;  -- Primary key should never be null
        
        IF @InvalidRecords > 0
        BEGIN
            RAISERROR('Audit log validation failed: %d records with NULL AuditID', 16, 1, @InvalidRecords);
        END
        ELSE
        BEGIN
            PRINT 'Audit log integrity verified: All ' + CAST(@TotalRecords AS VARCHAR(20)) + ' records valid';
        END
    END
    ELSE
    BEGIN
        PRINT 'audit.AuditLog table not found - skipping validation';
    END
    
    RETURN @InvalidRecords;
END;

GO

-- [bkp].[usp_CollectBackupHistory]
CREATE OR ALTER PROCEDURE bkp.usp_CollectBackupHistory 
    @DaysBack INT = 7
AS
BEGIN
    SET NOCOUNT ON;
    DECLARE @ServerName NVARCHAR(128) = @@SERVERNAME;
    
    INSERT INTO bkp.BackupHistory (ServerName, DatabaseName, BackupType, BackupStartDate, BackupFinishDate, DurationSeconds, BackupSizeMB, CompressedSizeMB, BackupPath, BackupSource)
    SELECT @ServerName, bs.database_name, bs.type, bs.backup_start_date, bs.backup_finish_date, DATEDIFF(SECOND, bs.backup_start_date, bs.backup_finish_date), bs.backup_size/1048576.0, bs.compressed_backup_size/1048576.0, bmf.physical_device_name, CASE WHEN bmf.physical_device_name LIKE '%Veeam%' THEN 'Veeam' ELSE 'Native' END
    FROM msdb.dbo.backupset bs INNER JOIN msdb.dbo.backupmediafamily bmf ON bs.media_set_id = bmf.media_set_id
    WHERE bs.backup_start_date >= DATEADD(DAY, -@DaysBack, GETDATE())
    AND NOT EXISTS (SELECT 1 FROM bkp.BackupHistory bh WHERE bh.ServerName = @ServerName AND bh.DatabaseName = bs.database_name AND bh.BackupStartDate = bs.backup_start_date);
END

GO

-- [bkp].[usp_EvaluateBackupCompliance]
CREATE OR ALTER PROCEDURE bkp.usp_EvaluateBackupCompliance
AS
BEGIN
    SET NOCOUNT ON;
    DECLARE @ServerName NVARCHAR(128) = @@SERVERNAME;
    DECLARE @Now DATETIME2 = SYSDATETIME();
    
    INSERT INTO bkp.BackupComplianceStatus (ServerName, DatabaseName, RecoveryModel, LastFullBackup, LastDiffBackup, LastLogBackup, FullBackupAgeHours, IsFullCompliant, IsDiffCompliant, IsLogCompliant, OverallCompliant, ComplianceScore)
    SELECT @ServerName, d.name, d.recovery_model_desc, LastFull.backup_finish_date, LastDiff.backup_finish_date, LastLog.backup_finish_date, DATEDIFF(HOUR, LastFull.backup_finish_date, @Now),
        CASE WHEN DATEDIFF(HOUR, LastFull.backup_finish_date, @Now) <= 168 THEN 1 ELSE 0 END,
        CASE WHEN DATEDIFF(HOUR, LastDiff.backup_finish_date, @Now) <= 24 OR LastDiff.backup_finish_date IS NULL THEN 1 ELSE 0 END,
        CASE WHEN d.recovery_model_desc = 'SIMPLE' OR DATEDIFF(MINUTE, LastLog.backup_finish_date, @Now) <= 30 OR LastLog.backup_finish_date IS NULL THEN 1 ELSE 0 END,
        CASE WHEN DATEDIFF(HOUR, LastFull.backup_finish_date, @Now) <= 168 THEN 1 ELSE 0 END,
        CASE WHEN DATEDIFF(HOUR, LastFull.backup_finish_date, @Now) <= 168 THEN 100 ELSE 0 END
    FROM sys.databases d
    OUTER APPLY (SELECT TOP 1 backup_finish_date FROM msdb.dbo.backupset WHERE database_name = d.name AND type = 'D' ORDER BY backup_finish_date DESC) LastFull
    OUTER APPLY (SELECT TOP 1 backup_finish_date FROM msdb.dbo.backupset WHERE database_name = d.name AND type = 'I' ORDER BY backup_finish_date DESC) LastDiff
    OUTER APPLY (SELECT TOP 1 backup_finish_date FROM msdb.dbo.backupset WHERE database_name = d.name AND type = 'L' ORDER BY backup_finish_date DESC) LastLog
    WHERE d.database_id > 4 AND d.state = 0;
END

GO

-- [capacity].[usp_CalculateGrowthForecast]
CREATE OR ALTER PROCEDURE capacity.usp_CalculateGrowthForecast
    @DaysHistory INT = 30
AS
BEGIN
    SET NOCOUNT ON;
    
    -- Create temp table for calculations
    CREATE TABLE #DiskCalc (
        ServerName NVARCHAR(128),
        DriveLetter CHAR(1),
        TotalSpace DECIMAL(18,2),
        CurrentUsed DECIMAL(18,2),
        MinUsed DECIMAL(18,2),
        MaxUsed DECIMAL(18,2),
        MinDate DATETIME2,
        MaxDate DATETIME2,
        RecordCount INT
    );
    
    -- Gather disk metrics
    INSERT INTO #DiskCalc
    SELECT 
        ServerName, 
        DriveLetter, 
        MAX(TotalSpaceGB),
        MAX(UsedSpaceGB),  -- Current (most recent will be max date)
        MIN(UsedSpaceGB),
        MAX(UsedSpaceGB),
        MIN(CollectedAt),
        MAX(CollectedAt),
        COUNT(*)
    FROM capacity.DiskSpaceHistory
    WHERE CollectedAt >= DATEADD(DAY, -@DaysHistory, GETDATE())
    GROUP BY ServerName, DriveLetter;
    
    -- Insert forecasts
    INSERT INTO capacity.GrowthForecast (ServerName, ResourceType, ResourceName, CurrentSizeGB, DailyGrowthGB, MonthlyGrowthGB, ThresholdGB, DaysToThreshold, ProjectedDateAtThreshold, ConfidencePercent)
    SELECT 
        ServerName, 
        'Disk', 
        DriveLetter, 
        CurrentUsed, 
        CASE WHEN RecordCount > 1 AND DATEDIFF(DAY, MinDate, MaxDate) > 0 
             THEN (MaxUsed - MinUsed) / DATEDIFF(DAY, MinDate, MaxDate) 
             ELSE 0 END AS DailyGrowth,
        CASE WHEN RecordCount > 1 AND DATEDIFF(DAY, MinDate, MaxDate) > 0 
             THEN ((MaxUsed - MinUsed) / DATEDIFF(DAY, MinDate, MaxDate)) * 30 
             ELSE 0 END AS MonthlyGrowth,
        TotalSpace * 0.90 AS ThresholdGB,
        CASE WHEN RecordCount > 1 AND DATEDIFF(DAY, MinDate, MaxDate) > 0 AND (MaxUsed - MinUsed) > 0
             THEN CAST((TotalSpace * 0.90 - CurrentUsed) / ((MaxUsed - MinUsed) / DATEDIFF(DAY, MinDate, MaxDate)) AS INT)
             ELSE NULL END AS DaysToThreshold,
        CASE WHEN RecordCount > 1 AND DATEDIFF(DAY, MinDate, MaxDate) > 0 AND (MaxUsed - MinUsed) > 0
             THEN DATEADD(DAY, CAST((TotalSpace * 0.90 - CurrentUsed) / ((MaxUsed - MinUsed) / DATEDIFF(DAY, MinDate, MaxDate)) AS INT), GETDATE())
             ELSE NULL END AS ProjectedDate,
        75.00
    FROM #DiskCalc
    WHERE RecordCount > 1 AND (MaxUsed - MinUsed) > 0;
    
    DROP TABLE #DiskCalc;
    
    PRINT 'Calculated growth forecasts';
END

GO

-- [capacity].[usp_CalculateGrowthTrends]

CREATE PROCEDURE capacity.usp_CalculateGrowthTrends
AS
BEGIN
    SET NOCOUNT ON;
    
    -- Calculate database growth trends
    ;WITH GrowthData AS (
        SELECT 
            ServerName,
            DatabaseName,
            CollectedDate,
            DataSizeMB,
            LAG(DataSizeMB) OVER (PARTITION BY ServerName, DatabaseName ORDER BY CollectedDate) AS PrevDataSizeMB,
            LAG(CollectedDate) OVER (PARTITION BY ServerName, DatabaseName ORDER BY CollectedDate) AS PrevDate
        FROM capacity.DatabaseGrowth
        WHERE CollectedDate > DATEADD(DAY, -30, GETDATE())
    )
    INSERT INTO capacity.DatabaseGrowth (ServerName, DatabaseName, DataSizeMB, CollectedDate)
    SELECT 
        ServerName,
        DatabaseName,
        AVG((DataSizeMB - PrevDataSizeMB) / NULLIF(DATEDIFF(DAY, PrevDate, CollectedDate), 0)) AS AvgDailyGrowthMB,
        GETDATE()
    FROM GrowthData
    WHERE PrevDataSizeMB IS NOT NULL
    GROUP BY ServerName, DatabaseName;
END

GO

-- [capacity].[usp_CollectDatabaseSizes]
CREATE OR ALTER PROCEDURE capacity.usp_CollectDatabaseSizes
AS
BEGIN
    SET NOCOUNT ON;
    DECLARE @ServerName NVARCHAR(128) = @@SERVERNAME;
    
    INSERT INTO capacity.DatabaseSizeHistory (ServerName, DatabaseName, DataSizeMB, LogSizeMB, TotalSizeMB)
    SELECT @ServerName, DB_NAME(database_id), SUM(CASE WHEN type=0 THEN size*8.0/1024 ELSE 0 END), SUM(CASE WHEN type=1 THEN size*8.0/1024 ELSE 0 END), SUM(size*8.0/1024)
    FROM sys.master_files GROUP BY database_id;
END

GO

-- [capacity].[usp_CollectDiskSpace]
CREATE OR ALTER PROCEDURE capacity.usp_CollectDiskSpace
AS
BEGIN
    SET NOCOUNT ON;
    DECLARE @ServerName NVARCHAR(128) = @@SERVERNAME;
    
    INSERT INTO capacity.DiskSpaceHistory (ServerName, DriveLetter, TotalSpaceGB, FreeSpaceGB, UsedSpaceGB, FreeSpacePercent)
    SELECT @ServerName, LEFT(volume_mount_point, 1), CAST(total_bytes/1073741824.0 AS DECIMAL(18,2)), CAST(available_bytes/1073741824.0 AS DECIMAL(18,2)), CAST((total_bytes-available_bytes)/1073741824.0 AS DECIMAL(18,2)), CAST(available_bytes*100.0/total_bytes AS DECIMAL(5,2))
    FROM sys.master_files mf CROSS APPLY sys.dm_os_volume_stats(mf.database_id, mf.file_id)
    GROUP BY volume_mount_point, total_bytes, available_bytes;
END

GO

-- [capacity].[usp_ForecastStorage]

CREATE   PROCEDURE capacity.usp_ForecastStorage
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @ForecastDays INT = 90;
    
    BEGIN TRY
        ;WITH DiskTrends AS (
            SELECT 
                ServerName,
                DriveLetter,
                AVG(TotalSpaceGB - FreeSpaceGB) AS AvgUsedGB,
                MAX(TotalSpaceGB - FreeSpaceGB) AS MaxUsedGB,
                MAX(TotalSpaceGB) AS TotalSpaceGB,
                CASE 
                    WHEN COUNT(*) > 1 AND DATEDIFF(DAY, MIN(CaptureDate), MAX(CaptureDate)) > 0
                    THEN (MAX(TotalSpaceGB - FreeSpaceGB) - MIN(TotalSpaceGB - FreeSpaceGB)) 
                         / NULLIF(DATEDIFF(DAY, MIN(CaptureDate), MAX(CaptureDate)), 0)
                    ELSE 0 
                END AS DailyGrowthGB,
                COUNT(*) AS DataPoints
            FROM fact.DiskSpaceMetrics
            WHERE CaptureDate >= DATEADD(DAY, -30, GETDATE())
            GROUP BY ServerName, DriveLetter
        )
        SELECT 
            ServerName,
            DriveLetter,
            TotalSpaceGB,
            MaxUsedGB AS CurrentUsedGB,
            TotalSpaceGB - MaxUsedGB AS CurrentFreeGB,
            CAST((MaxUsedGB * 100.0 / NULLIF(TotalSpaceGB, 0)) AS DECIMAL(5,2)) AS CurrentUsedPct,
            DailyGrowthGB,
            MaxUsedGB + (DailyGrowthGB * @ForecastDays) AS ProjectedUsedGB_90Days,
            CASE 
                WHEN DailyGrowthGB > 0 THEN CAST((TotalSpaceGB - MaxUsedGB) / DailyGrowthGB AS INT)
                ELSE 9999 
            END AS DaysUntilFull,
            CASE 
                WHEN DailyGrowthGB > 0 AND (TotalSpaceGB - MaxUsedGB) / DailyGrowthGB < 30 THEN 'CRITICAL'
                WHEN DailyGrowthGB > 0 AND (TotalSpaceGB - MaxUsedGB) / DailyGrowthGB < 90 THEN 'WARNING'
                ELSE 'OK'
            END AS ForecastStatus
        FROM DiskTrends
        WHERE TotalSpaceGB > 0
        ORDER BY DaysUntilFull ASC;
        
        PRINT 'Storage forecast completed.';
    END TRY
    BEGIN CATCH
        PRINT 'ERROR: ' + ERROR_MESSAGE();
        THROW;
    END CATCH
END;

GO

-- [change].[usp_CaptureSchemaChanges]

CREATE PROCEDURE change.usp_CaptureSchemaChanges
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @SQL NVARCHAR(MAX);
    DECLARE @LinkedServer NVARCHAR(256);
    DECLARE @LastCollection DATETIME;
    
    -- Get last collection time
    SELECT @LastCollection = ISNULL(MAX(ChangeTime), DATEADD(DAY, -1, GETDATE()))
    FROM change.SchemaChanges;
    
    DECLARE server_cursor CURSOR LOCAL FAST_FORWARD FOR
        SELECT ServerName FROM config.ServerInventory WHERE IsActive = 1;
    
    OPEN server_cursor;
    FETCH NEXT FROM server_cursor INTO @LinkedServer;
    
    WHILE @@FETCH_STATUS = 0
    BEGIN
        BEGIN TRY
            -- Collect from default trace if available
            SET @SQL = N'
            DECLARE @TraceFile NVARCHAR(500);
            SELECT @TraceFile = path FROM [' + @LinkedServer + '].master.sys.traces WHERE is_default = 1;
            
            IF @TraceFile IS NOT NULL
            BEGIN
                INSERT INTO DBAOpsRepository.change.SchemaChanges 
                    (ServerName, DatabaseName, SchemaName, ObjectName, ObjectType, ChangeType, ChangedBy, ChangeTime)
                SELECT DISTINCT
                    ''' + @LinkedServer + ''',
                    DatabaseName,
                    NULL,
                    ObjectName,
                    ObjectType,
                    CASE EventClass
                        WHEN 46 THEN ''CREATE''
                        WHEN 47 THEN ''DROP''
                        WHEN 164 THEN ''ALTER''
                    END,
                    LoginName,
                    StartTime
                FROM fn_trace_gettable(@TraceFile, DEFAULT) t
                WHERE EventClass IN (46, 47, 164)
                  AND StartTime > ''' + CONVERT(VARCHAR(30), @LastCollection, 120) + '''
                  AND DatabaseName NOT IN (''tempdb'', ''master'', ''model'', ''msdb'')
                  AND ObjectType NOT IN (''STATISTICS'');
            END';
            
            EXEC sp_executesql @SQL;
        END TRY
        BEGIN CATCH
            PRINT 'Error capturing schema changes from ' + @LinkedServer + ': ' + ERROR_MESSAGE();
        END CATCH
        
        FETCH NEXT FROM server_cursor INTO @LinkedServer;
    END
    
    CLOSE server_cursor;
    DEALLOCATE server_cursor;
END

GO

-- [change].[usp_GetTimelineByCMSGroup]

-- =============================================================================
-- PROCEDURE: change.usp_GetTimelineByCMSGroup
-- Get recent changes for a CMS group (dashboard)
-- =============================================================================
CREATE   PROCEDURE change.usp_GetTimelineByCMSGroup
    @CMSGroupPattern NVARCHAR(200) = '%',
    @DaysBack INT = 7,
    @TopN INT = 100
AS
BEGIN
    SET NOCOUNT ON;
    
    SELECT TOP (@TopN)
        t.TimelineID,
        ct.ChangeTypeName,
        ct.ChangeCategory,
        ct.Color,
        t.ServerName,
        t.CMSGroupName,
        t.CMSCriticality,
        t.ChangeDateTime,
        t.ChangeDescription,
        t.ChangedBy,
        t.ImpactLevel,
        t.IsPlanned
    FROM change.UnifiedTimeline t
    JOIN change.ChangeTypes ct ON t.ChangeTypeID = ct.ChangeTypeID
    WHERE t.ChangeDateTime >= DATEADD(DAY, -@DaysBack, GETDATE())
      AND t.CMSGroupName LIKE @CMSGroupPattern
    ORDER BY t.ChangeDateTime DESC;
END

GO

-- [change].[usp_GetTimelineForIncident]

-- =============================================================================
-- PROCEDURE: change.usp_GetTimelineForIncident
-- Get changes around the time of an incident (root cause analysis)
-- =============================================================================
CREATE   PROCEDURE change.usp_GetTimelineForIncident
    @IncidentID BIGINT = NULL,
    @ServerName NVARCHAR(128) = NULL,
    @IncidentDateTime DATETIME2 = NULL,
    @HoursBefore INT = 24,
    @HoursAfter INT = 4
AS
BEGIN
    SET NOCOUNT ON;
    
    -- Get incident details if ID provided
    IF @IncidentID IS NOT NULL AND @IncidentDateTime IS NULL
    BEGIN
        SELECT @IncidentDateTime = CreatedDate, @ServerName = PrimaryServerName
        FROM correlation.Incidents WHERE IncidentID = @IncidentID;
    END
    
    IF @IncidentDateTime IS NULL
        SET @IncidentDateTime = GETDATE();
    
    DECLARE @StartTime DATETIME2 = DATEADD(HOUR, -@HoursBefore, @IncidentDateTime);
    DECLARE @EndTime DATETIME2 = DATEADD(HOUR, @HoursAfter, @IncidentDateTime);
    
    SELECT 
        t.TimelineID,
        ct.ChangeTypeName,
        ct.ChangeCategory,
        ct.Icon,
        ct.Color,
        t.ServerName,
        t.CMSGroupName,
        t.CMSCriticality,
        t.DatabaseName,
        t.ChangeDateTime,
        t.ChangeDescription,
        t.PreviousValue,
        t.NewValue,
        t.ChangedBy,
        t.SourceSystem,
        t.SourceReference,
        t.ChangeControlNumber,
        t.ImpactLevel,
        t.IsPlanned,
        DATEDIFF(MINUTE, t.ChangeDateTime, @IncidentDateTime) AS MinutesBeforeIncident,
        CASE 
            WHEN t.ChangeDateTime < @IncidentDateTime THEN 'Before'
            ELSE 'After'
        END AS RelativeToIncident
    FROM change.UnifiedTimeline t
    JOIN change.ChangeTypes ct ON t.ChangeTypeID = ct.ChangeTypeID
    WHERE t.ChangeDateTime BETWEEN @StartTime AND @EndTime
      AND (@ServerName IS NULL OR t.ServerName = @ServerName)
    ORDER BY t.ChangeDateTime DESC;
END

GO

-- [change].[usp_RecordChange]

-- =============================================================================
-- PROCEDURE: change.usp_RecordChange
-- Universal procedure to record any change to the timeline
-- =============================================================================
CREATE   PROCEDURE change.usp_RecordChange
    @ChangeTypeName NVARCHAR(100),
    @ServerName NVARCHAR(128),
    @ChangeDescription NVARCHAR(MAX),
    @DatabaseName NVARCHAR(128) = NULL,
    @PreviousValue NVARCHAR(MAX) = NULL,
    @NewValue NVARCHAR(MAX) = NULL,
    @ChangedBy NVARCHAR(100) = NULL,
    @SourceSystem NVARCHAR(50) = 'Manual',
    @SourceReference NVARCHAR(200) = NULL,
    @ChangeControlNumber NVARCHAR(50) = NULL,
    @IsPlanned BIT = 0,
    @ChangeDateTime DATETIME2 = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    -- Get change type ID
    DECLARE @ChangeTypeID INT;
    SELECT @ChangeTypeID = ChangeTypeID FROM change.ChangeTypes WHERE ChangeTypeName = @ChangeTypeName;
    
    IF @ChangeTypeID IS NULL
    BEGIN
        -- Default to generic change type
        SELECT @ChangeTypeID = ChangeTypeID FROM change.ChangeTypes WHERE ChangeTypeName = 'Schema Change';
    END
    
    -- Get CMS info from config.ServerInventory (CMS-synced)
    DECLARE @CMSGroupName NVARCHAR(200), @CMSCriticality NVARCHAR(50);
    SELECT @CMSGroupName = CMSGroupName, @CMSCriticality = CMSCriticality
    FROM config.ServerInventory
    WHERE ServerName = @ServerName AND IsActive = 1;
    
    -- Default to function if not in inventory
    IF @CMSGroupName IS NULL
        SET @CMSGroupName = correlation.fn_GetCMSGroup(@ServerName);
    
    -- Get impact level from change type
    DECLARE @ImpactLevel TINYINT;
    SELECT @ImpactLevel = ImpactLevel FROM change.ChangeTypes WHERE ChangeTypeID = @ChangeTypeID;
    
    -- Insert timeline record
    INSERT INTO change.UnifiedTimeline (
        ChangeTypeID, ServerName, CMSGroupName, CMSCriticality, DatabaseName,
        ChangeDateTime, ChangeDescription, PreviousValue, NewValue, ChangedBy,
        SourceSystem, SourceReference, ChangeControlNumber, ImpactLevel, IsPlanned
    )
    VALUES (
        @ChangeTypeID, @ServerName, @CMSGroupName, @CMSCriticality, @DatabaseName,
        ISNULL(@ChangeDateTime, GETDATE()), @ChangeDescription, @PreviousValue, @NewValue, 
        ISNULL(@ChangedBy, SYSTEM_USER), @SourceSystem, @SourceReference, @ChangeControlNumber,
        @ImpactLevel, @IsPlanned
    );
    
    SELECT SCOPE_IDENTITY() AS TimelineID, 'Change recorded' AS Status;
END

GO

-- [change].[usp_RegisterDeployment]

-- =============================================================================
-- PROCEDURE: change.usp_RegisterDeployment
-- API endpoint for deployment registration (webhook target)
-- =============================================================================
CREATE   PROCEDURE change.usp_RegisterDeployment
    @ServerName NVARCHAR(128) = NULL,
    @DatabaseName NVARCHAR(128) = NULL,
    @ApplicationName NVARCHAR(200),
    @Version NVARCHAR(50),
    @PreviousVersion NVARCHAR(50) = NULL,
    @DeployedBy NVARCHAR(100) = NULL,
    @DeploymentSource NVARCHAR(100) = 'Manual',
    @TicketNumber NVARCHAR(50) = NULL,
    @ChangeControlNumber NVARCHAR(50) = NULL,
    @Description NVARCHAR(MAX) = NULL,
    @DeploymentGUID UNIQUEIDENTIFIER OUTPUT
AS
BEGIN
    SET NOCOUNT ON;
    
    -- Get CMS group
    DECLARE @CMSGroupName NVARCHAR(200) = correlation.fn_GetCMSGroup(@ServerName);
    
    -- Register deployment
    INSERT INTO change.DeploymentRegistry (
        ServerName, CMSGroupName, DatabaseName, ApplicationName, Version,
        PreviousVersion, DeployedBy, DeploymentSource, TicketNumber,
        ChangeControlNumber, Description
    )
    VALUES (
        @ServerName, @CMSGroupName, @DatabaseName, @ApplicationName, @Version,
        @PreviousVersion, ISNULL(@DeployedBy, SYSTEM_USER), @DeploymentSource,
        @TicketNumber, @ChangeControlNumber, @Description
    );
    
    SELECT @DeploymentGUID = DeploymentGUID 
    FROM change.DeploymentRegistry 
    WHERE DeploymentID = SCOPE_IDENTITY();
    
    -- Also record in unified timeline
    EXEC change.usp_RecordChange
        @ChangeTypeName = 'Application Deployment',
        @ServerName = @ServerName,
        @ChangeDescription = @Description,
        @DatabaseName = @DatabaseName,
        @PreviousValue = @PreviousVersion,
        @NewValue = @Version,
        @ChangedBy = @DeployedBy,
        @SourceSystem = @DeploymentSource,
        @SourceReference = @TicketNumber,
        @ChangeControlNumber = @ChangeControlNumber,
        @IsPlanned = 1;
    
    SELECT @DeploymentGUID AS DeploymentGUID, 'Deployment registered' AS Status;
END

GO

-- [change].[usp_TrackConfigChanges]

CREATE PROCEDURE change.usp_TrackConfigChanges
AS
BEGIN
    SET NOCOUNT ON;
    
    -- This would compare current config vs baseline
    -- For now, we track sp_configure changes via the DDL audit
    
    DECLARE @SQL NVARCHAR(MAX);
    DECLARE @LinkedServer NVARCHAR(256);
    
    DECLARE server_cursor CURSOR LOCAL FAST_FORWARD FOR
        SELECT ServerName FROM config.ServerInventory WHERE IsActive = 1;
    
    OPEN server_cursor;
    FETCH NEXT FROM server_cursor INTO @LinkedServer;
    
    WHILE @@FETCH_STATUS = 0
    BEGIN
        BEGIN TRY
            -- Capture current configuration
            SET @SQL = N'
            INSERT INTO DBAOpsRepository.change.ConfigurationChanges 
                (ServerName, ConfigName, NewValue, ChangeTime)
            SELECT 
                ''' + @LinkedServer + ''',
                name,
                CAST(value_in_use AS NVARCHAR(256)),
                GETDATE()
            FROM [' + @LinkedServer + '].master.sys.configurations
            WHERE name NOT IN (
                SELECT ConfigName FROM DBAOpsRepository.change.ConfigurationChanges 
                WHERE ServerName = ''' + @LinkedServer + '''
                  AND ChangeTime > DATEADD(DAY, -1, GETDATE())
            )';
            
            EXEC sp_executesql @SQL;
        END TRY
        BEGIN CATCH
            PRINT 'Error tracking config from ' + @LinkedServer + ': ' + ERROR_MESSAGE();
        END CATCH
        
        FETCH NEXT FROM server_cursor INTO @LinkedServer;
    END
    
    CLOSE server_cursor;
    DEALLOCATE server_cursor;
END

GO

-- [compliance].[usp_CollectEvidence]

-- =============================================================================
-- PROCEDURE: compliance.usp_CollectEvidence
-- Collect and store compliance evidence
-- =============================================================================
CREATE   PROCEDURE compliance.usp_CollectEvidence
    @Framework NVARCHAR(50),
    @ControlID NVARCHAR(50),
    @ServerName NVARCHAR(128) = NULL,
    @DatabaseName NVARCHAR(128) = NULL,
    @EvidenceType NVARCHAR(50) = 'Query',
    @EvidenceData NVARCHAR(MAX),
    @ComplianceStatus NVARCHAR(50),
    @Findings NVARCHAR(MAX) = NULL,
    @ValidDays INT = 30
AS
BEGIN
    SET NOCOUNT ON;
    
    -- Get control name
    DECLARE @ControlName NVARCHAR(500);
    SELECT @ControlName = ControlName FROM compliance.ControlDefinitions
    WHERE Framework = @Framework AND ControlID = @ControlID;
    
    -- Get CMS info
    DECLARE @CMSGroupName NVARCHAR(200), @CMSCriticality NVARCHAR(50);
    IF @ServerName IS NOT NULL
    BEGIN
        SELECT @CMSGroupName = CMSGroupName, @CMSCriticality = CMSCriticality
        FROM config.ServerInventory
        WHERE ServerName = @ServerName AND IsActive = 1;
    END
    
    -- Generate integrity hash
    DECLARE @Hash VARBINARY(32) = HASHBYTES('SHA2_256', @EvidenceData);
    
    -- Insert evidence
    INSERT INTO compliance.EvidenceStore (
        Framework, ControlID, ControlName, ServerName, CMSGroupName, CMSCriticality,
        DatabaseName, EvidenceType, EvidenceData, EvidenceHash, ComplianceStatus,
        Findings, ValidUntil
    )
    VALUES (
        @Framework, @ControlID, @ControlName, @ServerName, @CMSGroupName, @CMSCriticality,
        @DatabaseName, @EvidenceType, @EvidenceData, @Hash, @ComplianceStatus,
        @Findings, DATEADD(DAY, @ValidDays, GETDATE())
    );
    
    SELECT SCOPE_IDENTITY() AS EvidenceID, 'Evidence collected' AS Status;
END

GO

-- [compliance].[usp_CollectJobCompliance]

-- Create the job compliance procedure
CREATE   PROCEDURE compliance.usp_CollectJobCompliance
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @Now DATETIME2 = SYSDATETIME();
    DECLARE @ViolationCount INT = 0;
    
    BEGIN TRY
        -- Create temp table for violations
        CREATE TABLE #JobViolations (
            ServerName SYSNAME,
            JobName SYSNAME,
            LastRunDate DATETIME,
            LastRunStatus INT,
            StatusDesc VARCHAR(20),
            HoursSinceLastRun INT,
            ViolationType VARCHAR(50)
        );
        
        -- Check for failed jobs in last 24 hours
        INSERT INTO #JobViolations
        SELECT 
            @@SERVERNAME AS ServerName,
            j.name AS JobName,
            msdb.dbo.agent_datetime(h.run_date, h.run_time) AS LastRunDate,
            h.run_status AS LastRunStatus,
            CASE h.run_status
                WHEN 0 THEN 'Failed'
                WHEN 1 THEN 'Succeeded'
                WHEN 2 THEN 'Retry'
                WHEN 3 THEN 'Canceled'
            END AS StatusDesc,
            DATEDIFF(HOUR, msdb.dbo.agent_datetime(h.run_date, h.run_time), @Now) AS HoursSinceLastRun,
            'Failed Job' AS ViolationType
        FROM msdb.dbo.sysjobs j
        INNER JOIN msdb.dbo.sysjobhistory h ON j.job_id = h.job_id
        WHERE h.step_id = 0  -- Job outcome
          AND h.run_status = 0  -- Failed
          AND msdb.dbo.agent_datetime(h.run_date, h.run_time) > DATEADD(HOUR, -24, @Now)
          AND j.enabled = 1;
        
        -- Check for enabled jobs that haven't run in expected timeframe
        INSERT INTO #JobViolations
        SELECT 
            @@SERVERNAME AS ServerName,
            j.name AS JobName,
            msdb.dbo.agent_datetime(h.run_date, h.run_time) AS LastRunDate,
            h.run_status AS LastRunStatus,
            'Stale' AS StatusDesc,
            DATEDIFF(HOUR, msdb.dbo.agent_datetime(h.run_date, h.run_time), @Now) AS HoursSinceLastRun,
            'Stale Job - No Recent Run' AS ViolationType
        FROM msdb.dbo.sysjobs j
        LEFT JOIN (
            SELECT job_id, MAX(instance_id) AS max_instance
            FROM msdb.dbo.sysjobhistory
            WHERE step_id = 0
            GROUP BY job_id
        ) latest ON j.job_id = latest.job_id
        LEFT JOIN msdb.dbo.sysjobhistory h ON latest.job_id = h.job_id 
            AND latest.max_instance = h.instance_id
        INNER JOIN msdb.dbo.sysjobschedules js ON j.job_id = js.job_id
        WHERE j.enabled = 1
          AND (h.run_date IS NULL 
               OR msdb.dbo.agent_datetime(h.run_date, h.run_time) < DATEADD(DAY, -2, @Now))
          AND j.name NOT LIKE '%syspolicy%'
          AND j.name NOT LIKE '%SSIS%';
        
        SET @ViolationCount = (SELECT COUNT(*) FROM #JobViolations);
        
        -- Log to fact table if it exists
        IF OBJECT_ID('fact.JobComplianceLog', 'U') IS NOT NULL
        BEGIN
            INSERT INTO fact.JobComplianceLog (
                ServerName, JobName, LastRunDate, LastRunStatus, 
                HoursSinceLastRun, ViolationType, CheckTime
            )
            SELECT 
                ServerName, JobName, LastRunDate, LastRunStatus,
                HoursSinceLastRun, ViolationType, @Now
            FROM #JobViolations;
        END
        
        -- Return violations (for job output)
        IF @ViolationCount > 0
        BEGIN
            SELECT * FROM #JobViolations ORDER BY ViolationType, JobName;
            
            -- Raise error to trigger alert step
            DECLARE @Msg NVARCHAR(500) = CONCAT('Job Compliance: ', @ViolationCount, ' violation(s) found.');
            RAISERROR(@Msg, 16, 1);
        END
        ELSE
        BEGIN
            PRINT 'Job Compliance Check: All jobs compliant.';
        END
        
        DROP TABLE #JobViolations;
        
    END TRY
    BEGIN CATCH
        IF OBJECT_ID('tempdb..#JobViolations') IS NOT NULL
            DROP TABLE #JobViolations;
            
        THROW;
    END CATCH
END

GO

-- [compliance].[usp_CreateSnapshot]

-- =============================================================================
-- PROCEDURE: compliance.usp_CreateSnapshot
-- Create point-in-time audit snapshot
-- =============================================================================
CREATE   PROCEDURE compliance.usp_CreateSnapshot
    @SnapshotName NVARCHAR(200),
    @Framework NVARCHAR(50),
    @CMSGroupScope NVARCHAR(200) = NULL,
    @Description NVARCHAR(MAX) = NULL,
    @AuditPeriodStart DATE = NULL,
    @AuditPeriodEnd DATE = NULL,
    @SnapshotID INT OUTPUT
AS
BEGIN
    SET NOCOUNT ON;
    
    IF @AuditPeriodStart IS NULL SET @AuditPeriodStart = DATEADD(MONTH, -1, GETDATE());
    IF @AuditPeriodEnd IS NULL SET @AuditPeriodEnd = GETDATE();
    
    -- Calculate stats
    DECLARE @Total INT, @Pass INT, @Fail INT, @Exception INT;
    
    SELECT 
        @Total = COUNT(DISTINCT ControlID),
        @Pass = COUNT(DISTINCT CASE WHEN ComplianceStatus = 'Pass' THEN ControlID END),
        @Fail = COUNT(DISTINCT CASE WHEN ComplianceStatus = 'Fail' THEN ControlID END),
        @Exception = COUNT(DISTINCT CASE WHEN ComplianceStatus = 'Exception' THEN ControlID END)
    FROM compliance.EvidenceStore
    WHERE Framework = @Framework
      AND CollectionDateTime BETWEEN @AuditPeriodStart AND @AuditPeriodEnd
      AND (@CMSGroupScope IS NULL OR CMSGroupName LIKE @CMSGroupScope)
      AND IsArchived = 0;
    
    INSERT INTO compliance.AuditSnapshots (
        SnapshotName, SnapshotDescription, Framework, CMSGroupScope,
        AuditPeriodStart, AuditPeriodEnd, TotalControls, PassingControls,
        FailingControls, ExceptionControls
    )
    VALUES (
        @SnapshotName, @Description, @Framework, @CMSGroupScope,
        @AuditPeriodStart, @AuditPeriodEnd, @Total, @Pass, @Fail, @Exception
    );
    
    SET @SnapshotID = SCOPE_IDENTITY();
    
    SELECT @SnapshotID AS SnapshotID, 
           @Total AS TotalControls,
           @Pass AS PassingControls,
           CAST(@Pass * 100.0 / NULLIF(@Total, 0) AS DECIMAL(5,2)) AS ComplianceScore;
END

GO

-- [compliance].[usp_ExportEvidencePackage]

-- =============================================================================
-- PROCEDURE: compliance.usp_ExportEvidencePackage
-- Export evidence for auditors (JSON format)
-- =============================================================================
CREATE   PROCEDURE compliance.usp_ExportEvidencePackage
    @Framework NVARCHAR(50),
    @ControlIDs NVARCHAR(MAX) = NULL,
    @CMSGroupPattern NVARCHAR(200) = '%',
    @StartDate DATE = NULL,
    @EndDate DATE = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    IF @StartDate IS NULL SET @StartDate = DATEADD(MONTH, -3, GETDATE());
    IF @EndDate IS NULL SET @EndDate = GETDATE();
    
    SELECT 
        EvidenceGUID,
        Framework,
        ControlID,
        ControlName,
        ServerName,
        CMSGroupName,
        CMSCriticality,
        DatabaseName,
        CollectionDateTime,
        EvidenceType,
        EvidenceData,
        CONVERT(VARCHAR(64), EvidenceHash, 2) AS EvidenceHashHex,
        ComplianceStatus,
        Findings,
        CollectedBy,
        ValidUntil
    FROM compliance.EvidenceStore
    WHERE Framework = @Framework
      AND CollectionDateTime BETWEEN @StartDate AND @EndDate
      AND CMSGroupName LIKE @CMSGroupPattern
      AND (@ControlIDs IS NULL OR ControlID IN (SELECT TRIM(value) FROM STRING_SPLIT(@ControlIDs, ',')))
      AND IsArchived = 0
    ORDER BY ControlID, CollectionDateTime DESC
    FOR JSON PATH, ROOT('EvidencePackage');
END

GO

-- [compliance].[usp_GenerateAuditReport]

-- =============================================================================
-- PROCEDURE: compliance.usp_GenerateAuditReport
-- Generate comprehensive audit report for a framework
-- =============================================================================
CREATE   PROCEDURE compliance.usp_GenerateAuditReport
    @Framework NVARCHAR(50),
    @CMSGroupPattern NVARCHAR(200) = '%',
    @StartDate DATE = NULL,
    @EndDate DATE = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    IF @StartDate IS NULL SET @StartDate = DATEADD(MONTH, -1, GETDATE());
    IF @EndDate IS NULL SET @EndDate = GETDATE();
    
    -- Summary
    SELECT 
        @Framework AS Framework,
        @StartDate AS PeriodStart,
        @EndDate AS PeriodEnd,
        COUNT(DISTINCT e.ControlID) AS ControlsEvaluated,
        COUNT(DISTINCT e.ServerName) AS ServersEvaluated,
        SUM(CASE WHEN e.ComplianceStatus = 'Pass' THEN 1 ELSE 0 END) AS PassCount,
        SUM(CASE WHEN e.ComplianceStatus = 'Fail' THEN 1 ELSE 0 END) AS FailCount,
        SUM(CASE WHEN e.ComplianceStatus = 'Warning' THEN 1 ELSE 0 END) AS WarningCount,
        SUM(CASE WHEN e.ComplianceStatus = 'Exception' THEN 1 ELSE 0 END) AS ExceptionCount,
        CAST(SUM(CASE WHEN e.ComplianceStatus = 'Pass' THEN 1.0 ELSE 0 END) / 
             NULLIF(COUNT(*), 0) * 100 AS DECIMAL(5,2)) AS ComplianceScore
    FROM compliance.EvidenceStore e
    WHERE e.Framework = @Framework
      AND e.CollectionDateTime BETWEEN @StartDate AND @EndDate
      AND e.CMSGroupName LIKE @CMSGroupPattern
      AND e.IsArchived = 0;
    
    -- Control detail
    SELECT 
        e.ControlID,
        cd.ControlName,
        cd.Category,
        cd.Severity,
        e.ComplianceStatus,
        COUNT(*) AS EvidenceCount,
        MAX(e.CollectionDateTime) AS LastCollected,
        MAX(e.Findings) AS LatestFindings
    FROM compliance.EvidenceStore e
    LEFT JOIN compliance.ControlDefinitions cd 
        ON e.Framework = cd.Framework AND e.ControlID = cd.ControlID
    WHERE e.Framework = @Framework
      AND e.CollectionDateTime BETWEEN @StartDate AND @EndDate
      AND e.CMSGroupName LIKE @CMSGroupPattern
      AND e.IsArchived = 0
    GROUP BY e.ControlID, cd.ControlName, cd.Category, cd.Severity, e.ComplianceStatus
    ORDER BY cd.Severity, e.ControlID;
END

GO

-- [config].[usp_ListSQLPorts]
CREATE OR ALTER PROCEDURE [config].[usp_ListSQLPorts]
AS
BEGIN
    SET NOCOUNT ON;

    SELECT 
        @@SERVERNAME AS CurrentServer,
        local_net_address,
        local_tcp_port,
        client_net_address
    FROM sys.dm_exec_connections
    WHERE local_net_address IS NOT NULL
      AND local_tcp_port IS NOT NULL;
END

GO

-- [config].[usp_RefreshServerInventory]

CREATE PROCEDURE config.usp_RefreshServerInventory
AS
BEGIN
    SET NOCOUNT ON;
    
    -- Update LastSeen timestamp for all active servers we can reach
    UPDATE config.ServerInventory
    SET LastSeen = SYSDATETIME()
    WHERE IsActive = 1;
    
    -- Also update key server properties if available
    UPDATE si
    SET 
        SQLVersion = CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(50)),
        SQLEdition = CAST(SERVERPROPERTY('Edition') AS NVARCHAR(100))
    FROM config.ServerInventory si
    WHERE si.ServerName = @@SERVERNAME;
    
    PRINT 'Server inventory refreshed: ' + CAST(@@ROWCOUNT AS VARCHAR(10)) + ' servers updated';
END;

GO

-- [config].[usp_SaveMaintenanceWindow]

CREATE PROCEDURE config.usp_SaveMaintenanceWindow
    @WindowID INT = NULL,
    @WindowName NVARCHAR(100),
    @Description NVARCHAR(500) = NULL,
    @Environment NVARCHAR(50) = NULL,
    @DayOfWeek INT = 1,
    @WeekOfMonth INT = 0,
    @StartTime TIME,
    @DurationMinutes INT = 240,
    @IsActive BIT = 1,
    @ComplianceTier NVARCHAR(100) = NULL,
    @RequiresApproval BIT = 0,
    @AutoReboot BIT = 0,
    @ModifiedBy NVARCHAR(100) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    IF @WindowID IS NULL OR @WindowID = 0
    BEGIN
        INSERT INTO config.MaintenanceWindows (
            WindowName, [Description], Environment, DayOfWeek, WeekOfMonth,
            StartTime, DurationMinutes, IsActive, ComplianceTier, RequiresApproval, AutoReboot
        )
        VALUES (
            @WindowName, @Description, @Environment, @DayOfWeek, @WeekOfMonth,
            @StartTime, @DurationMinutes, @IsActive, @ComplianceTier, @RequiresApproval, @AutoReboot
        );
        
        SET @WindowID = SCOPE_IDENTITY();
        SELECT @WindowID AS WindowID, 'Created' AS Action;
    END
    ELSE
    BEGIN
        UPDATE config.MaintenanceWindows
        SET WindowName = @WindowName,
            [Description] = @Description,
            Environment = @Environment,
            DayOfWeek = @DayOfWeek,
            WeekOfMonth = @WeekOfMonth,
            StartTime = @StartTime,
            DurationMinutes = @DurationMinutes,
            IsActive = @IsActive,
            ComplianceTier = @ComplianceTier,
            RequiresApproval = @RequiresApproval,
            AutoReboot = @AutoReboot
        WHERE WindowID = @WindowID;
        
        SELECT @WindowID AS WindowID, 'Updated' AS Action;
    END
END

GO

-- [config].[usp_Send_JobSuccess_Alert]

-- Create stub replacements that do nothing (for any remaining callers)
CREATE   PROCEDURE config.usp_Send_JobSuccess_Alert
(
    @JobName     NVARCHAR(255),
    @StartTime   DATETIME,
    @EndTime     DATETIME
)
AS
BEGIN
    -- SUCCESS ALERTS DISABLED - Dashboard shows status
    -- This procedure intentionally does nothing
    PRINT 'DBAOps | JB: Success alert suppressed for ' + @JobName + '. Dashboard shows status.';
END;

GO

-- [config].[usp_Send_JobSuccess_Alert_DEPRECATED]

CREATE   PROCEDURE config.usp_Send_JobSuccess_Alert
(
      @JobName     NVARCHAR(255),
      @StartTime   DATETIME,
      @EndTime     DATETIME
)
AS
BEGIN
    SET NOCOUNT ON;

    --------------------------------------------------------------------
    -- BUILD HTML BODY SAFELY (NO UNICODE DASHES!)
    --------------------------------------------------------------------
    DECLARE @Body NVARCHAR(MAX);
    DECLARE @Subject NVARCHAR(255);

    SET @Body =
        N'<h2 style="color:green;">SUCCESS - ' + @JobName + N'</h2>' +
        N'<p>The job completed successfully.</p>' +
        N'<b>StartTime:</b> ' + CONVERT(NVARCHAR(30), @StartTime, 120) + N'<br>' +
        N'<b>EndTime:</b> '   + CONVERT(NVARCHAR(30), @EndTime,   120) + N'<br><br>';

    SET @Subject = N'*** DBAOps SUCCESS: ' + @JobName + N' ***';

    --------------------------------------------------------------------
    -- SEND EMAIL
    --------------------------------------------------------------------
    EXEC msdb.dbo.sp_send_dbmail
          @profile_name = 'DBAOpsProfile',
          @recipients   = 'SQLServerAdminNotification@YOURORG.gov',
          @subject      = @Subject,
          @body         = @Body,
          @body_format  = 'HTML';
END

GO

-- [config].[usp_Send_WindowsUpdateComplianceAlert]

CREATE   PROCEDURE config.usp_Send_WindowsUpdateComplianceAlert
AS
BEGIN
    SET NOCOUNT ON;

    -------------------------------------------------------
    -- Pull list of non-compliant servers
    -------------------------------------------------------
    DECLARE @NonCompliant TABLE (
        ServerName NVARCHAR(128),
        AllowMUUpdateService INT,
        LastChecked DATETIME,
        Notes NVARCHAR(MAX)
    );

    INSERT INTO @NonCompliant
    SELECT 
        ServerName,
        AllowMUUpdateService,
        LastChecked,
        Notes
    FROM config.WindowsUpdateCompliance
    WHERE AllowMUUpdateService IS NULL
       OR AllowMUUpdateService <> 0;

    -------------------------------------------------------
    -- If everything is compliant, exit quietly
    -------------------------------------------------------
    IF NOT EXISTS (SELECT 1 FROM @NonCompliant)
    BEGIN
        RETURN;
    END

    -------------------------------------------------------
    -- Build email body
    -------------------------------------------------------
    DECLARE @Body NVARCHAR(MAX);

    SET @Body = 
    'WINDOWS UPDATE COMPLIANCE ALERT

One or more SQL Servers have the Windows Update setting 
"Allow updates for other Microsoft products" ENABLED or UNKNOWN.

This may cause SQL Server patches to be deployed during Windows OS patching,
breaking patching governance and violating DBAOps maintenance windows.

Non-Compliant Servers:

' +
    (
        SELECT 
            'Server: ' + ServerName +
            '  MU Setting: ' + ISNULL(CAST(AllowMUUpdateService AS NVARCHAR(10)), 'NULL') +
            '  Last Checked: ' + CONVERT(VARCHAR(19), LastChecked, 120) +
            COALESCE('  Notes: ' + Notes, '') + CHAR(10)
        FROM @NonCompliant
        FOR XML PATH(''), TYPE
    ).value('.', 'NVARCHAR(MAX)');

    -------------------------------------------------------
    -- Send email
    -------------------------------------------------------
    EXEC msdb.dbo.sp_send_dbmail
        @profile_name = 'DBAOpsProfile',
        @recipients   = 'SQLServerAdminNotification@YOURORG.gov; DBAOps@YOURORG.gov',
        @subject      = 'Windows Update Compliance Alert â€“ Non-Compliant SQL Servers Detected',
        @body         = @Body,
        @body_format  = 'TEXT';
END

GO

-- [config].[usp_SendReachabilityAlerts]

CREATE PROCEDURE config.usp_SendReachabilityAlerts  
AS  
BEGIN  
    SET NOCOUNT ON;  
  
    ----------------------------------------------------------------------  
    -- 1. Find servers that are unreachable in the last 10 minutes  
    ----------------------------------------------------------------------  
    ;WITH LatestCheck AS  
    (  
        SELECT   
            ServerName,  
            Reachable,  
            ErrorMessage,  
            CheckedOn,  
            ROW_NUMBER() OVER (PARTITION BY ServerName ORDER BY CheckedOn DESC) AS rn  
        FROM config.ServerReachability  
    )  
    SELECT   
        ServerName,  
        ErrorMessage,  
        CheckedOn  
    INTO #Unreachable  
    FROM LatestCheck  
    WHERE rn = 1  
      AND Reachable = 0  
      AND CheckedOn >= DATEADD(MINUTE, -15, GETDATE());
        
  
    ----------------------------------------------------------------------  
    -- 2. Remove servers we've already emailed (avoid duplicates)  
    ----------------------------------------------------------------------  
    DELETE U  
    FROM #Unreachable U  
    WHERE EXISTS (  
        SELECT 1   
        FROM config.ReachabilityAlertsLog L  
        WHERE L.ServerName = U.ServerName  
          AND L.AlertSentOn >= DATEADD(HOUR, -2, GETDATE())
    );  
  
    ----------------------------------------------------------------------  
    -- Exit if nothing to send  
    ----------------------------------------------------------------------  
    IF NOT EXISTS (SELECT 1 FROM #Unreachable)  
    BEGIN  
        RETURN;  
    END  
  
  
    ----------------------------------------------------------------------  
    -- 3. Build HTML Email Body  
    ----------------------------------------------------------------------  
    DECLARE @Html NVARCHAR(MAX);  
  
    SET @Html =   
    N'<h2>ðŸš¨ SQL Server Reachability Alert</h2>' +  
    N'<p>The following SQL Servers have become unreachable:</p>' +  
    N'<table border="1" cellpadding="6" cellspacing="0" style="border-collapse:collapse;">' +  
    N'<tr style="background-color:#f2f2f2;">' +  
    N'<th>Server Name</th><th>Error Message</th><th>Checked On</th></tr>';  
  
    SELECT @Html = @Html +  
        N'<tr>' +  
        N'<td>' + ServerName + '</td>' +  
        N'<td>' + ISNULL(ErrorMessage, 'Unknown Error') + '</td>' +  
        N'<td>' + CONVERT(VARCHAR(30), CheckedOn, 120) + '</td>' +  
        N'</tr>'  
    FROM #Unreachable;  
  
    SET @Html = @Html + N'</table><br/><p>Please investigate immediately.</p>';  
  
  
    ----------------------------------------------------------------------  
    -- 4. Send Email (FIXED: Changed from SQLAlertsProfile to DBAOpsProfile)
    ----------------------------------------------------------------------  
    EXEC msdb.dbo.sp_send_dbmail  
        @profile_name = 'DBAOpsProfile',   -- FIXED!
        @recipients = 'SQLServerAdminNotification@YOURORG.gov',  
        @subject = 'ðŸš¨ SQL Server Reachability Alert â€“ One or More Servers Offline',  
        @body = @Html,  
        @body_format = 'HTML';  
  
  
    ----------------------------------------------------------------------  
    -- 5. Log Sent Alerts  
    ----------------------------------------------------------------------  
    INSERT INTO config.ReachabilityAlertsLog (ServerName, ErrorMessage)  
    SELECT ServerName, ErrorMessage  
    FROM #Unreachable;  
  
END

GO

-- [config].[usp_SyncFromCMS]

CREATE PROCEDURE config.usp_SyncFromCMS
    @DebugMode BIT = 0
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @InsertedA2 INT = 0, @ReactivatedA2 INT = 0, @InsertedDim INT = 0;
    DECLARE @UpdatedConfig INT = 0, @InsertedConfig INT = 0, @DeactivatedConfig INT = 0;
    DECLARE @CMSCount INT = 0;
    DECLARE @SyncDate DATETIME2 = SYSDATETIME();

    IF OBJECT_ID('tempdb..#CMSServers') IS NOT NULL DROP TABLE #CMSServers;

    ;WITH RankedCMS AS (
        SELECT 
            s.server_name,
            g.server_group_id,
            g.name AS GroupName,
            pg.name AS ParentGroupName,
            pg.server_group_id AS ParentGroupID,
            ROW_NUMBER() OVER (
                PARTITION BY s.server_name 
                ORDER BY 
                    CASE pg.name 
                        WHEN '01_PRODUCTION' THEN 1 
                        WHEN '02_NONPRODUCTION' THEN 2 
                        WHEN '04_INFRASTRUCTURE' THEN 3 
                        WHEN '03_REPLICATION' THEN 4 
                        ELSE 5 
                    END,
                    CASE g.name
                        WHEN 'HIGH_CRITICALITY' THEN 1
                        WHEN 'MEDIUM_CRITICALITY' THEN 2
                        WHEN 'LOW_CRITICALITY' THEN 3
                        ELSE 4
                    END
            ) AS rn
        FROM msdb.dbo.sysmanagement_shared_registered_servers_internal s
        JOIN msdb.dbo.sysmanagement_shared_server_groups_internal g 
            ON s.server_group_id = g.server_group_id
        LEFT JOIN msdb.dbo.sysmanagement_shared_server_groups_internal pg 
            ON g.parent_id = pg.server_group_id
    )
    SELECT 
        server_name AS ServerName,
        server_name + '.internal.YOURORG.ad' AS FQDN,
        server_group_id AS CMSGroupID,
        GroupName AS CMSGroupName,
        ParentGroupName AS CMSParentGroupName,
        ParentGroupID AS CMSParentGroupID,
        CASE 
            WHEN ParentGroupName = '01_PRODUCTION' THEN 'PROD'
            WHEN ParentGroupName = '02_NONPRODUCTION' THEN 'NON-PROD'
            WHEN ParentGroupName = '04_INFRASTRUCTURE' THEN 'INFRASTRUCTURE'
            WHEN ParentGroupName = '03_REPLICATION' THEN 'REPLICATION'
            ELSE 'Unknown'
        END AS CMSEnvironment,
        CASE 
            WHEN GroupName = 'HIGH_CRITICALITY' THEN 'HIGH'
            WHEN GroupName = 'MEDIUM_CRITICALITY' THEN 'MEDIUM'
            WHEN GroupName = 'LOW_CRITICALITY' THEN 'LOW'
            WHEN GroupName = 'MONITORING_SQL' THEN 'INFRASTRUCTURE'
            WHEN GroupName = 'REPORTING_SQL' THEN 'INFRASTRUCTURE'
            WHEN GroupName = 'PUBLISHERS' THEN 'REPLICATION'
            WHEN GroupName = 'NONPROD_APP' THEN 'NON-PROD'
            ELSE 'Unknown'
        END AS CMSCriticality
    INTO #CMSServers
    FROM RankedCMS
    WHERE rn = 1;

    SELECT @CMSCount = COUNT(*) FROM #CMSServers;

    IF @DebugMode = 1
    BEGIN
        PRINT 'CMS unique servers: ' + CAST(@CMSCount AS VARCHAR);
        SELECT * FROM #CMSServers ORDER BY ServerName;
    END

    -- ========== config.ServerInventory ==========
    INSERT INTO config.ServerInventory (
        ServerName, FQDN, Environment, IsActive, IsSQLServer,
        CMSServerGroupID, CMSGroupName, CMSParentGroupID, 
        CMSCriticality, CMSEnvironment, CMSLastSyncDate, CMSParentGroupName,
        SourceSystem, CreatedDate, ModifiedDate
    )
    SELECT 
        cms.ServerName, cms.FQDN, cms.CMSEnvironment, 1, 1,
        cms.CMSGroupID, cms.CMSGroupName, cms.CMSParentGroupID,
        cms.CMSCriticality, cms.CMSEnvironment, @SyncDate, cms.CMSParentGroupName,
        'CMS', @SyncDate, @SyncDate
    FROM #CMSServers cms
    WHERE NOT EXISTS (SELECT 1 FROM config.ServerInventory si WHERE si.ServerName = cms.ServerName);
    SET @InsertedConfig = @@ROWCOUNT;

    UPDATE si SET
        si.CMSServerGroupID = cms.CMSGroupID, si.CMSGroupName = cms.CMSGroupName,
        si.CMSParentGroupID = cms.CMSParentGroupID, si.CMSCriticality = cms.CMSCriticality,
        si.CMSEnvironment = cms.CMSEnvironment, si.CMSLastSyncDate = @SyncDate,
        si.CMSParentGroupName = cms.CMSParentGroupName, si.ModifiedDate = @SyncDate, si.IsActive = 1
    FROM config.ServerInventory si
    INNER JOIN #CMSServers cms ON si.ServerName = cms.ServerName;
    SET @UpdatedConfig = @@ROWCOUNT;

    UPDATE config.ServerInventory SET
        IsActive = 0, ModifiedDate = @SyncDate,
        Notes = ISNULL(Notes, '') + ' | Deactivated by CMS sync ' + CONVERT(VARCHAR(30), @SyncDate, 120)
    WHERE ServerName NOT IN (SELECT ServerName FROM #CMSServers) AND IsActive = 1;
    SET @DeactivatedConfig = @@ROWCOUNT;

    -- ========== dbo.A2: INSERT new ==========
    INSERT INTO dbo.A2 (
        Server, FQDN, InstanceName, DomainName, Environment,
        IsActive, IsProcessed, IsSQLServer, AddedBy, SourceSystem, 
        CreateDate, LastUpdated, MetricsPopulatedSuccessfully
    )
    SELECT 
        cms.ServerName, cms.FQDN, cms.ServerName, 'INTERNAL', cms.CMSEnvironment,
        1, 0, 1, 'CMS Sync', 'CMS', @SyncDate, @SyncDate, 0
    FROM #CMSServers cms
    WHERE NOT EXISTS (SELECT 1 FROM dbo.A2 a WHERE a.Server = cms.ServerName);
    SET @InsertedA2 = @@ROWCOUNT;

    -- ========== dbo.A2: REACTIVATE ==========
    UPDATE dbo.A2 SET
        IsActive = 1, LastUpdated = @SyncDate,
        Notes = ISNULL(Notes, '') + ' | Reactivated by CMS sync ' + CONVERT(VARCHAR(30), @SyncDate, 120)
    WHERE IsActive = 0
        AND Server IN (SELECT ServerName FROM #CMSServers);
    SET @ReactivatedA2 = @@ROWCOUNT;

    -- ========== dbo.dim_Servers ==========
    INSERT INTO dbo.dim_Servers (ServerName)
    SELECT cms.ServerName
    FROM #CMSServers cms
    WHERE NOT EXISTS (SELECT 1 FROM dbo.dim_Servers d WHERE d.ServerName = cms.ServerName);
    SET @InsertedDim = @@ROWCOUNT;

    PRINT '========================================';
    PRINT '  CMS SYNC COMPLETE';
    PRINT '========================================';
    PRINT '  CMS unique servers    : ' + CAST(@CMSCount AS VARCHAR);
    PRINT '  config.ServerInventory: ' + CAST(@InsertedConfig AS VARCHAR) + ' inserted, ' 
          + CAST(@UpdatedConfig AS VARCHAR) + ' updated, ' 
          + CAST(@DeactivatedConfig AS VARCHAR) + ' deactivated';
    PRINT '  dbo.A2               : ' + CAST(@InsertedA2 AS VARCHAR) + ' inserted, '
          + CAST(@ReactivatedA2 AS VARCHAR) + ' reactivated';
    PRINT '  dbo.dim_Servers      : ' + CAST(@InsertedDim AS VARCHAR) + ' inserted';
    PRINT '========================================';

    SELECT 'CMS Sync' AS Operation, @CMSCount AS CMSUniqueServers,
        @InsertedConfig AS ConfigInserted, @UpdatedConfig AS ConfigUpdated,
        @DeactivatedConfig AS ConfigDeactivated, 
        @InsertedA2 AS A2Inserted, @ReactivatedA2 AS A2Reactivated,
        @InsertedDim AS DimServersInserted, @SyncDate AS SyncTimestamp;

    DROP TABLE #CMSServers;
END

GO

-- [config].[usp_SyncServerInventoryFromCMS]

CREATE PROCEDURE config.usp_SyncServerInventoryFromCMS
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @Inserted INT = 0;
    DECLARE @Deactivated INT = 0;
    DECLARE @Reactivated INT = 0;
    DECLARE @CMSUpdated INT = 0;
    
    BEGIN TRY
        BEGIN TRANSACTION;
        
        -- 1. Add new servers from CMS that don't exist in ServerInventory
        INSERT INTO config.ServerInventory (
            ServerName, 
            IsActive, 
            IsSQLServer,
            SourceSystem,
            CreatedDate, 
            ModifiedDate,
            CMSLastSyncDate
        )
        SELECT DISTINCT 
            cms.server_name,
            1,              -- IsActive
            1,              -- IsSQLServer (assumed since it's in CMS)
            'CMS',          -- SourceSystem
            GETDATE(),
            GETDATE(),
            GETDATE()
        FROM msdb.dbo.sysmanagement_shared_registered_servers_internal cms
        WHERE NOT EXISTS (
            SELECT 1 FROM config.ServerInventory si 
            WHERE UPPER(si.ServerName) = UPPER(cms.server_name)
        );
        
        SET @Inserted = @@ROWCOUNT;
        
        -- 2. Deactivate servers that are no longer in CMS
        UPDATE config.ServerInventory
        SET IsActive = 0,
            ModifiedDate = GETDATE(),
            CMSLastSyncDate = GETDATE()
        WHERE IsActive = 1
        AND SourceSystem = 'CMS'  -- Only deactivate CMS-sourced servers
        AND NOT EXISTS (
            SELECT 1 FROM msdb.dbo.sysmanagement_shared_registered_servers_internal cms
            WHERE UPPER(cms.server_name) = UPPER(ServerInventory.ServerName)
        );
        
        SET @Deactivated = @@ROWCOUNT;
        
        -- 3. Reactivate servers that are back in CMS
        UPDATE config.ServerInventory
        SET IsActive = 1,
            ModifiedDate = GETDATE(),
            CMSLastSyncDate = GETDATE()
        WHERE IsActive = 0
        AND EXISTS (
            SELECT 1 FROM msdb.dbo.sysmanagement_shared_registered_servers_internal cms
            WHERE UPPER(cms.server_name) = UPPER(ServerInventory.ServerName)
        );
        
        SET @Reactivated = @@ROWCOUNT;
        
        -- 4. Update CMS metadata for all servers in CMS
        UPDATE si
        SET 
            CMSServerGroupID = cms.server_group_id,
            CMSGroupName = g.name,
            CMSParentGroupID = g.parent_id,
            CMSLastSyncDate = GETDATE(),
            ModifiedDate = GETDATE()
        FROM config.ServerInventory si
        JOIN msdb.dbo.sysmanagement_shared_registered_servers_internal cms
            ON UPPER(si.ServerName) = UPPER(cms.server_name)
        LEFT JOIN msdb.dbo.sysmanagement_shared_server_groups_internal g
            ON cms.server_group_id = g.server_group_id
        WHERE si.CMSServerGroupID IS NULL 
           OR si.CMSServerGroupID <> cms.server_group_id
           OR si.CMSLastSyncDate IS NULL
           OR si.CMSLastSyncDate < DATEADD(HOUR, -1, GETDATE());
        
        SET @CMSUpdated = @@ROWCOUNT;
        
        COMMIT TRANSACTION;
        
        -- Log results
        PRINT 'DBAOps | JB: CMS Sync Complete';
        PRINT '  - Servers Added: ' + CAST(@Inserted AS VARCHAR(10));
        PRINT '  - Servers Deactivated: ' + CAST(@Deactivated AS VARCHAR(10));
        PRINT '  - Servers Reactivated: ' + CAST(@Reactivated AS VARCHAR(10));
        PRINT '  - CMS Metadata Updated: ' + CAST(@CMSUpdated AS VARCHAR(10));
        
        -- Return summary
        SELECT 
            @Inserted AS ServersAdded,
            @Deactivated AS ServersDeactivated,
            @Reactivated AS ServersReactivated,
            @CMSUpdated AS CMSMetadataUpdated,
            (SELECT COUNT(*) FROM config.ServerInventory WHERE IsActive = 1) AS TotalActiveServers,
            (SELECT COUNT(DISTINCT server_name) FROM msdb.dbo.sysmanagement_shared_registered_servers_internal) AS CMSServerCount;
            
    END TRY
    BEGIN CATCH
        IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
        THROW;
    END CATCH;
END;

GO

-- [control].[usp_EvaluateExternalProcessThrottling]

CREATE   PROCEDURE control.usp_EvaluateExternalProcessThrottling
    @DebugMode BIT = 0
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @Now DATETIME2 = GETDATE();
    DECLARE @ActionsExecuted INT = 0;
    
    IF @DebugMode = 1
        PRINT 'Starting External Process Throttling Evaluation at ' + CONVERT(VARCHAR, @Now, 120);
    
    -- Create temp table for evaluation results
    CREATE TABLE #ThrottleEvaluation (
        ConfigID INT,
        ServerKey INT,
        Server VARCHAR(100),
        ProcessName VARCHAR(128),
        CurrentState VARCHAR(20),
        ThrottledPriority VARCHAR(20),
        NormalPriority VARCHAR(20),
        ThrottledCPUAffinity BIGINT,
        AvgSQLCPU DECIMAL(5,2),
        ThrottleThreshold DECIMAL(5,2),
        RestoreThreshold DECIMAL(5,2),
        RequiredAction VARCHAR(20)
    );
    
    -- Check if we have CPU data
    IF EXISTS (SELECT 1 FROM fact.CPUPerformance WHERE CollectionTime > DATEADD(MINUTE, -15, @Now))
    BEGIN
        -- Evaluate each configured server/process using actual CPU data
        INSERT INTO #ThrottleEvaluation
        SELECT 
            e.ConfigID,
            e.ServerKey,
            e.Server,
            e.ProcessName,
            e.CurrentState,
            e.ThrottledPriority,
            e.NormalPriority,
            e.ThrottledCPUAffinity,
            cpu.AvgCPU,
            e.ThrottleWhenSQLCPUAbove,
            e.RestoreWhenSQLCPUBelow,
            CASE 
                WHEN cpu.AvgCPU > e.ThrottleWhenSQLCPUAbove AND e.CurrentState = 'Normal' 
                    THEN 'THROTTLE'
                WHEN cpu.AvgCPU < e.RestoreWhenSQLCPUBelow AND e.CurrentState = 'Throttled' 
                    THEN 'RESTORE'
                ELSE 'NONE'
            END AS RequiredAction
        FROM control.ExternalProcessConfig e
        INNER JOIN dim.DimServer ds ON e.ServerKey = ds.ServerKey AND ds.IsCurrent = 1
        CROSS APPLY (
            SELECT AVG(p.SQLServerCPUPercent) AS AvgCPU
            FROM fact.CPUPerformance p
            WHERE p.ServerKey = ds.ServerKey
              AND p.CollectionTime > DATEADD(MINUTE, -e.EvaluationWindowMinutes, @Now)
        ) cpu
        WHERE e.ThrottleEnabled = 1
          AND cpu.AvgCPU IS NOT NULL;
    END
    ELSE
    BEGIN
        IF @DebugMode = 1
            PRINT 'No recent CPU data found. Skipping evaluation.';
    END
    
    IF @DebugMode = 1
    BEGIN
        PRINT 'Evaluation Results:';
        SELECT * FROM #ThrottleEvaluation WHERE RequiredAction <> 'NONE';
    END
    
    -- Log actions to history
    INSERT INTO meta.ExternalProcessThrottleHistory (
        ServerName, ProcessName, Action, PreviousState, NewState,
        PreviousPriority, NewPriority, SQLCPUAtAction, ThresholdValue
    )
    SELECT 
        Server, 
        ProcessName, 
        RequiredAction,
        CurrentState,
        CASE RequiredAction 
            WHEN 'THROTTLE' THEN 'Throttled' 
            WHEN 'RESTORE' THEN 'Normal'
        END,
        CASE RequiredAction 
            WHEN 'THROTTLE' THEN NormalPriority 
            WHEN 'RESTORE' THEN ThrottledPriority
        END,
        CASE RequiredAction 
            WHEN 'THROTTLE' THEN ThrottledPriority 
            WHEN 'RESTORE' THEN NormalPriority
        END,
        AvgSQLCPU,
        CASE RequiredAction 
            WHEN 'THROTTLE' THEN ThrottleThreshold 
            WHEN 'RESTORE' THEN RestoreThreshold
        END
    FROM #ThrottleEvaluation
    WHERE RequiredAction IN ('THROTTLE', 'RESTORE');
    
    SET @ActionsExecuted = @@ROWCOUNT;
    
    -- Update configuration state
    UPDATE e
    SET 
        e.CurrentState = CASE t.RequiredAction 
            WHEN 'THROTTLE' THEN 'Throttled' 
            WHEN 'RESTORE' THEN 'Normal' 
        END,
        e.LastStateChange = @Now,
        e.LastEvaluationTime = @Now,
        e.ConsecutiveThrottleCount = CASE t.RequiredAction 
            WHEN 'THROTTLE' THEN e.ConsecutiveThrottleCount + 1 
            ELSE 0 
        END,
        e.ModifiedDate = @Now
    FROM control.ExternalProcessConfig e
    INNER JOIN #ThrottleEvaluation t ON e.ConfigID = t.ConfigID
    WHERE t.RequiredAction IN ('THROTTLE', 'RESTORE');
    
    -- Update evaluation time for all processed configs
    UPDATE e
    SET e.LastEvaluationTime = @Now
    FROM control.ExternalProcessConfig e
    INNER JOIN #ThrottleEvaluation t ON e.ConfigID = t.ConfigID;
    
    -- Return actions for PowerShell execution
    SELECT 
        Server AS ServerName,
        ProcessName,
        RequiredAction,
        AvgSQLCPU AS CurrentSQLCPU,
        CASE WHEN RequiredAction = 'THROTTLE' THEN ThrottledPriority ELSE NormalPriority END AS TargetPriority,
        CASE WHEN RequiredAction = 'THROTTLE' THEN ThrottledCPUAffinity ELSE NULL END AS TargetAffinity
    FROM #ThrottleEvaluation
    WHERE RequiredAction IN ('THROTTLE', 'RESTORE')
    ORDER BY Server, ProcessName;
    
    DROP TABLE #ThrottleEvaluation;
    
    IF @DebugMode = 1
        PRINT 'Completed. Actions executed: ' + CAST(@ActionsExecuted AS VARCHAR);
    
    RETURN @ActionsExecuted;
END;

GO

-- [control].[usp_ManualThrottleOverride]

-- Manual override procedure
CREATE   PROCEDURE control.usp_ManualThrottleOverride
    @Server VARCHAR(100),
    @ProcessName VARCHAR(128) = NULL,
    @Action VARCHAR(20),
    @Reason VARCHAR(500) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    IF @Action NOT IN ('THROTTLE', 'RESTORE')
    BEGIN
        RAISERROR('Action must be THROTTLE or RESTORE', 16, 1);
        RETURN;
    END
    
    INSERT INTO meta.ExternalProcessThrottleHistory (
        ServerName, ProcessName, Action, PreviousState, NewState,
        ActionBy, ExecutionResult
    )
    SELECT 
        Server,
        ProcessName,
        @Action,
        CurrentState,
        CASE @Action WHEN 'THROTTLE' THEN 'Throttled' ELSE 'Normal' END,
        SYSTEM_USER + ' (Manual: ' + ISNULL(@Reason, 'No reason') + ')',
        'Manual'
    FROM control.ExternalProcessConfig
    WHERE Server = @Server
      AND (ProcessName = @ProcessName OR @ProcessName IS NULL);
    
    UPDATE control.ExternalProcessConfig
    SET CurrentState = CASE @Action WHEN 'THROTTLE' THEN 'Throttled' ELSE 'Normal' END,
        LastStateChange = GETDATE(),
        ModifiedDate = GETDATE(),
        ModifiedBy = SYSTEM_USER
    WHERE Server = @Server
      AND (ProcessName = @ProcessName OR @ProcessName IS NULL);
    
    PRINT 'Manual ' + @Action + ' applied to ' + CAST(@@ROWCOUNT AS VARCHAR) + ' process(es)';
END;

GO

-- [control].[usp_UpdateThrottleExecutionResult]

-- Procedure to update execution results
CREATE   PROCEDURE control.usp_UpdateThrottleExecutionResult
    @ServerName NVARCHAR(128),
    @ProcessName NVARCHAR(128),
    @ExecutionResult NVARCHAR(20),
    @ErrorMessage NVARCHAR(MAX) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    UPDATE meta.ExternalProcessThrottleHistory
    SET ExecutionResult = @ExecutionResult,
        ErrorMessage = @ErrorMessage
    WHERE ServerName = @ServerName
      AND ProcessName = @ProcessName
      AND ActionTime = (
          SELECT MAX(ActionTime) 
          FROM meta.ExternalProcessThrottleHistory 
          WHERE ServerName = @ServerName AND ProcessName = @ProcessName
      );
END;

GO

-- [correlation].[usp_AcknowledgeIncident]

-- =============================================================================
-- PROCEDURE: correlation.usp_AcknowledgeIncident
-- =============================================================================
CREATE   PROCEDURE correlation.usp_AcknowledgeIncident
    @IncidentID BIGINT,
    @AcknowledgedBy NVARCHAR(100),
    @TrackITTicket NVARCHAR(50) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    UPDATE correlation.Incidents
    SET Status = 'Acknowledged',
        AcknowledgedDate = GETDATE(),
        AcknowledgedBy = @AcknowledgedBy,
        TrackITTicket = ISNULL(@TrackITTicket, TrackITTicket)
    WHERE IncidentID = @IncidentID AND Status = 'Open';
    
    SELECT @@ROWCOUNT AS RowsAffected, 
           CASE WHEN @@ROWCOUNT > 0 THEN 'Acknowledged' ELSE 'Not found or already acknowledged' END AS Result;
END

GO

-- [correlation].[usp_CorrelateAlerts]

CREATE PROCEDURE correlation.usp_CorrelateAlerts
    @ProcessMinutes INT = 60,
    @Debug BIT = 0
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @ProcessedCount INT = 0;
    DECLARE @NewIncidents INT = 0;
    DECLARE @CorrelatedAlerts INT = 0;
    DECLARE @SuppressedCount INT = 0;
    DECLARE @CutoffTime DATETIME2 = DATEADD(MINUTE, -@ProcessMinutes, GETDATE());
    
    IF @Debug = 1
    BEGIN
        PRINT '=== Correlation Engine Started ===';
        PRINT CONCAT('Processing window: ', @ProcessMinutes, ' minutes');
        PRINT CONCAT('Cutoff time: ', CONVERT(VARCHAR(30), @CutoffTime, 121));
        SELECT COUNT(*) AS UnprocessedAlerts FROM correlation.AlertQueue WHERE IsProcessed = 0;
    END
    
    BEGIN TRY
        BEGIN TRANSACTION;
        
        -- =====================================================================
        -- STEP 1: Generate fingerprints for unprocessed alerts
        -- =====================================================================
        DECLARE @AlertID BIGINT, @AlertType VARCHAR(100), @SourceServer VARCHAR(256);
        DECLARE @SourceDatabase VARCHAR(256), @AlertSeverity TINYINT;
        DECLARE @FingerprintHash VARBINARY(32), @FingerprintID BIGINT;
        DECLARE @ShouldSuppress BIT, @SuppressionReason NVARCHAR(200);
        
        DECLARE alert_cursor CURSOR LOCAL FAST_FORWARD FOR
            SELECT AlertQueueID, AlertType, SourceServer, SourceDatabase, Severity
            FROM correlation.AlertQueue
            WHERE IsProcessed = 0
              AND ReceivedDate >= @CutoffTime
            ORDER BY ReceivedDate;
        
        OPEN alert_cursor;
        FETCH NEXT FROM alert_cursor INTO @AlertID, @AlertType, @SourceServer, @SourceDatabase, @AlertSeverity;
        
        WHILE @@FETCH_STATUS = 0
        BEGIN
            -- Generate fingerprint
            EXEC correlation.usp_GenerateFingerprint
                @AlertType = @AlertType,
                @ServerName = @SourceServer,
                @DatabaseName = @SourceDatabase,
                @FingerprintHash = @FingerprintHash OUTPUT,
                @FingerprintID = @FingerprintID OUTPUT;
            
            -- Check if should suppress
            EXEC correlation.usp_ShouldSuppress
                @AlertType = @AlertType,
                @ServerName = @SourceServer,
                @FingerprintHash = @FingerprintHash,
                @ShouldSuppress = @ShouldSuppress OUTPUT,
                @SuppressionReason = @SuppressionReason OUTPUT;
            
            IF @ShouldSuppress = 1
            BEGIN
                -- Mark as processed but suppressed
                UPDATE correlation.AlertQueue
                SET IsProcessed = 1,
                    ProcessedDate = GETDATE(),
                    FingerprintHash = CONVERT(VARCHAR(64), @FingerprintHash, 2)
                WHERE AlertQueueID = @AlertID;
                
                SET @SuppressedCount = @SuppressedCount + 1;
                
                IF @Debug = 1
                    PRINT CONCAT('Suppressed alert ', @AlertID, ': ', @SuppressionReason);
            END
            ELSE
            BEGIN
                -- Update alert with fingerprint
                UPDATE correlation.AlertQueue
                SET FingerprintHash = CONVERT(VARCHAR(64), @FingerprintHash, 2)
                WHERE AlertQueueID = @AlertID;
            END
            
            FETCH NEXT FROM alert_cursor INTO @AlertID, @AlertType, @SourceServer, @SourceDatabase, @AlertSeverity;
        END
        
        CLOSE alert_cursor;
        DEALLOCATE alert_cursor;
        
        -- =====================================================================
        -- STEP 2: Apply correlation rules to create/update incidents
        -- =====================================================================
        DECLARE @RuleID INT, @RuleName NVARCHAR(200), @AlertTypePattern NVARCHAR(400);
        DECLARE @CMSGroupPattern NVARCHAR(400), @TimeWindowMinutes INT, @MinAlertCount INT;
        
        DECLARE rule_cursor CURSOR LOCAL FAST_FORWARD FOR
            SELECT RuleID, RuleName, AlertTypePattern, CMSGroupPattern, TimeWindowMinutes, MinAlertCount
            FROM correlation.CorrelationRules
            WHERE IsEnabled = 1
            ORDER BY Priority;
        
        OPEN rule_cursor;
        FETCH NEXT FROM rule_cursor INTO @RuleID, @RuleName, @AlertTypePattern, @CMSGroupPattern, @TimeWindowMinutes, @MinAlertCount;
        
        WHILE @@FETCH_STATUS = 0
        BEGIN
            -- Find alerts matching this rule that aren't yet in an incident
            DECLARE @MatchingAlerts TABLE (
                AlertQueueID BIGINT,
                SourceServer VARCHAR(256),
                CMSGroup NVARCHAR(200),
                AlertType VARCHAR(100),
                Severity TINYINT
            );
            
            DELETE FROM @MatchingAlerts;
            
            INSERT INTO @MatchingAlerts
            SELECT 
                aq.AlertQueueID,
                aq.SourceServer,
                correlation.fn_GetCMSGroup(aq.SourceServer) AS CMSGroup,
                aq.AlertType,
                aq.Severity
            FROM correlation.AlertQueue aq
            WHERE aq.IsProcessed = 0
              AND aq.ReceivedDate >= DATEADD(MINUTE, -@TimeWindowMinutes, GETDATE())
              AND aq.AlertType LIKE ISNULL(@AlertTypePattern, '%')
              AND aq.IncidentID IS NULL;
            
            -- Filter by CMS group if pattern specified
            IF @CMSGroupPattern IS NOT NULL
                DELETE FROM @MatchingAlerts WHERE CMSGroup NOT LIKE @CMSGroupPattern;
            
            -- Check if we have enough alerts to create an incident
            DECLARE @AlertCount INT;
            SELECT @AlertCount = COUNT(*) FROM @MatchingAlerts;
            
            IF @AlertCount >= @MinAlertCount
            BEGIN
                -- Create new incident
                DECLARE @NewIncidentID BIGINT;
                DECLARE @PrimaryServer VARCHAR(256);
                DECLARE @CMSGroupName NVARCHAR(200);
                DECLARE @MaxSeverity TINYINT;
                DECLARE @AffectedServerList NVARCHAR(MAX);
                
                SELECT TOP 1 
                    @PrimaryServer = SourceServer,
                    @CMSGroupName = CMSGroup,
                    @MaxSeverity = (SELECT MIN(Severity) FROM @MatchingAlerts)
                FROM @MatchingAlerts;
                
                -- Build affected servers list (compatible with older SQL Server)
                SET @AffectedServerList = NULL;
                SELECT @AffectedServerList = COALESCE(@AffectedServerList + ', ', '') + SourceServer
                FROM (SELECT DISTINCT SourceServer FROM @MatchingAlerts WHERE SourceServer IS NOT NULL) AS Servers;
                
                INSERT INTO correlation.Incidents (
                    Title,
                    Description,
                    Severity,
                    Status,
                    PrimaryServerName,
                    AffectedServers,
                    CMSGroupName,
                    AlertCount,
                    CorrelationRuleID
                )
                VALUES (
                    CONCAT(@RuleName, ' - ', @PrimaryServer),
                    CONCAT('Correlated ', @AlertCount, ' alerts matching rule: ', @RuleName),
                    @MaxSeverity,
                    'Open',
                    @PrimaryServer,
                    @AffectedServerList,
                    @CMSGroupName,
                    @AlertCount,
                    @RuleID
                );
                
                SET @NewIncidentID = SCOPE_IDENTITY();
                SET @NewIncidents = @NewIncidents + 1;
                
                -- Link alerts to incident
                INSERT INTO correlation.IncidentAlerts (IncidentID, AlertID, FingerprintID, AlertDateTime, AlertType, ServerName, IsRootCause)
                SELECT 
                    @NewIncidentID,
                    ma.AlertQueueID,
                    f.FingerprintID,
                    aq.ReceivedDate,
                    ma.AlertType,
                    ma.SourceServer,
                    CASE WHEN ma.AlertQueueID = (SELECT MIN(AlertQueueID) FROM @MatchingAlerts) THEN 1 ELSE 0 END
                FROM @MatchingAlerts ma
                INNER JOIN correlation.AlertQueue aq ON ma.AlertQueueID = aq.AlertQueueID
                LEFT JOIN correlation.Fingerprints f ON CONVERT(VARCHAR(64), f.FingerprintHash, 2) = aq.FingerprintHash;
                
                -- Update alerts as processed
                UPDATE aq
                SET IsProcessed = 1,
                    ProcessedDate = GETDATE(),
                    IncidentID = @NewIncidentID
                FROM correlation.AlertQueue aq
                INNER JOIN @MatchingAlerts ma ON aq.AlertQueueID = ma.AlertQueueID;
                
                SET @CorrelatedAlerts = @CorrelatedAlerts + @AlertCount;
                
                IF @Debug = 1
                    PRINT CONCAT('Created incident ', @NewIncidentID, ' (', @RuleName, ') with ', @AlertCount, ' alerts');
            END
            
            FETCH NEXT FROM rule_cursor INTO @RuleID, @RuleName, @AlertTypePattern, @CMSGroupPattern, @TimeWindowMinutes, @MinAlertCount;
        END
        
        CLOSE rule_cursor;
        DEALLOCATE rule_cursor;
        
        -- =====================================================================
        -- STEP 3: Process remaining alerts (create individual incidents for critical)
        -- =====================================================================
        INSERT INTO correlation.Incidents (
            Title,
            Description,
            Severity,
            Status,
            PrimaryServerName,
            CMSGroupName,
            AlertCount
        )
        SELECT 
            CONCAT(aq.AlertType, ' on ', ISNULL(aq.SourceServer, 'Unknown')),
            LEFT(aq.AlertMessage, 1000),
            aq.Severity,
            'Open',
            aq.SourceServer,
            correlation.fn_GetCMSGroup(aq.SourceServer),
            1
        FROM correlation.AlertQueue aq
        WHERE aq.IsProcessed = 0
          AND aq.ReceivedDate >= @CutoffTime
          AND aq.Severity <= 2;  -- Only create incidents for Critical (1) and High (2)
        
        SET @NewIncidents = @NewIncidents + @@ROWCOUNT;
        
        -- Mark remaining unprocessed alerts as processed
        UPDATE aq
        SET IsProcessed = 1,
            ProcessedDate = GETDATE()
        FROM correlation.AlertQueue aq
        WHERE aq.IsProcessed = 0
          AND aq.ReceivedDate >= @CutoffTime;
        
        SET @ProcessedCount = @CorrelatedAlerts + @SuppressedCount + @@ROWCOUNT;
        
        COMMIT TRANSACTION;
        
        -- =====================================================================
        -- Summary output
        -- =====================================================================
        PRINT '=== Correlation Engine Complete ===';
        PRINT CONCAT('Alerts processed: ', @ProcessedCount);
        PRINT CONCAT('Alerts suppressed: ', @SuppressedCount);
        PRINT CONCAT('Alerts correlated: ', @CorrelatedAlerts);
        PRINT CONCAT('New incidents created: ', @NewIncidents);
        
        IF @Debug = 1
        BEGIN
            SELECT 
                'Open Incidents' AS Summary,
                COUNT(*) AS Total,
                SUM(CASE WHEN Severity = 1 THEN 1 ELSE 0 END) AS Critical,
                SUM(CASE WHEN Severity = 2 THEN 1 ELSE 0 END) AS High
            FROM correlation.Incidents
            WHERE Status = 'Open';
        END
        
    END TRY
    BEGIN CATCH
        IF @@TRANCOUNT > 0
            ROLLBACK TRANSACTION;
        
        DECLARE @ErrorMessage NVARCHAR(4000) = ERROR_MESSAGE();
        DECLARE @ErrorSeverity INT = ERROR_SEVERITY();
        DECLARE @ErrorState INT = ERROR_STATE();
        
        PRINT CONCAT('ERROR in usp_CorrelateAlerts: ', @ErrorMessage);
        
        RAISERROR(@ErrorMessage, @ErrorSeverity, @ErrorState);
    END CATCH
END

GO

-- [correlation].[usp_GenerateFingerprint]

-- =============================================================================
-- PROCEDURE: correlation.usp_GenerateFingerprint
-- =============================================================================
CREATE   PROCEDURE correlation.usp_GenerateFingerprint
    @AlertType NVARCHAR(100),
    @ServerName NVARCHAR(128) = NULL,
    @DatabaseName NVARCHAR(128) = NULL,
    @MetricName NVARCHAR(100) = NULL,
    @ThresholdValue DECIMAL(18,4) = NULL,
    @FingerprintHash VARBINARY(32) OUTPUT,
    @FingerprintID BIGINT OUTPUT
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @FingerprintString NVARCHAR(MAX) = CONCAT(
        ISNULL(@AlertType, ''), '|',
        ISNULL(@ServerName, ''), '|',
        ISNULL(@DatabaseName, ''), '|',
        ISNULL(@MetricName, ''), '|',
        ISNULL(CAST(@ThresholdValue AS NVARCHAR(50)), '')
    );
    
    SET @FingerprintHash = HASHBYTES('SHA2_256', @FingerprintString);
    
    SELECT @FingerprintID = FingerprintID
    FROM correlation.Fingerprints
    WHERE FingerprintHash = @FingerprintHash;
    
    IF @FingerprintID IS NULL
    BEGIN
        INSERT INTO correlation.Fingerprints (FingerprintHash, AlertType, ServerName, DatabaseName, MetricName, ThresholdValue)
        VALUES (@FingerprintHash, @AlertType, @ServerName, @DatabaseName, @MetricName, @ThresholdValue);
        SET @FingerprintID = SCOPE_IDENTITY();
    END
    ELSE
    BEGIN
        UPDATE correlation.Fingerprints
        SET LastSeenDate = GETDATE(), OccurrenceCount = OccurrenceCount + 1
        WHERE FingerprintID = @FingerprintID;
    END
END

GO

-- [correlation].[usp_ShouldSuppress]

-- =============================================================================
-- PROCEDURE: correlation.usp_ShouldSuppress
-- =============================================================================
CREATE   PROCEDURE correlation.usp_ShouldSuppress
    @AlertType NVARCHAR(100),
    @ServerName NVARCHAR(128) = NULL,
    @FingerprintHash VARBINARY(32) = NULL,
    @ShouldSuppress BIT OUTPUT,
    @SuppressionReason NVARCHAR(200) OUTPUT
AS
BEGIN
    SET NOCOUNT ON;
    SET @ShouldSuppress = 0;
    SET @SuppressionReason = NULL;
    
    DECLARE @CMSGroup NVARCHAR(200) = correlation.fn_GetCMSGroup(@ServerName);
    
    -- Check fingerprint deduplication
    IF @FingerprintHash IS NOT NULL
    BEGIN
        DECLARE @LastSeen DATETIME2, @SuppressionMinutes INT;
        
        SELECT @LastSeen = LastSeenDate, @SuppressionMinutes = SuppressionWindowMinutes
        FROM correlation.Fingerprints
        WHERE FingerprintHash = @FingerprintHash AND IsActive = 1;
        
        IF @LastSeen IS NOT NULL AND DATEDIFF(MINUTE, @LastSeen, GETDATE()) < @SuppressionMinutes
        BEGIN
            SET @ShouldSuppress = 1;
            SET @SuppressionReason = CONCAT('Deduplicated (', DATEDIFF(MINUTE, @LastSeen, GETDATE()), ' min ago)');
            RETURN;
        END
    END
    
    -- Check maintenance windows (by CMS group)
    IF EXISTS (
        SELECT 1 FROM correlation.SuppressionRules
        WHERE IsEnabled = 1 AND GETDATE() BETWEEN StartTime AND EndTime
          AND (@AlertType LIKE AlertTypePattern OR AlertTypePattern IS NULL)
          AND (@ServerName LIKE ServerPattern OR ServerPattern IS NULL)
          AND (@CMSGroup LIKE CMSGroupPattern OR CMSGroupPattern IS NULL)
    )
    BEGIN
        SELECT TOP 1 @SuppressionReason = CONCAT('Maintenance: ', RuleName)
        FROM correlation.SuppressionRules
        WHERE IsEnabled = 1 AND GETDATE() BETWEEN StartTime AND EndTime;
        SET @ShouldSuppress = 1;
    END
END

GO

-- [Credentials].[usp_GetMigrationDashboard]

-- ============================================================
-- Procedure: Get Migration Dashboard
-- ============================================================
CREATE   PROCEDURE [Credentials].[usp_GetMigrationDashboard]
AS
BEGIN
    SET NOCOUNT ON;
    
    -- Staging Status
    PRINT 'STAGING STATUS:'
    SELECT ProcessingStatus, COUNT(*) AS Count
    FROM [Credentials].[KeePassMigrationStaging]
    GROUP BY ProcessingStatus
    
    -- Inventory by Type
    PRINT ''
    PRINT 'INVENTORY BY TYPE:'
    SELECT ct.TypeName, COUNT(*) AS Count
    FROM [Credentials].[CredentialInventory] ci
    JOIN [Credentials].[CredentialType] ct ON ci.CredentialTypeID = ct.CredentialTypeID
    WHERE ci.IsActive = 1
    GROUP BY ct.TypeName
    
    -- Inventory by Vault
    PRINT ''
    PRINT 'INVENTORY BY VAULT:'
    SELECT vt.VaultName, COUNT(*) AS Count
    FROM [Credentials].[CredentialInventory] ci
    JOIN [Credentials].[VaultType] vt ON ci.VaultTypeID = vt.VaultTypeID
    WHERE ci.IsActive = 1
    GROUP BY vt.VaultName
END

GO

-- [Credentials].[usp_GetSyncDashboard]

-- ============================================================
-- Procedure: Get Sync Dashboard
-- ============================================================
CREATE   PROCEDURE [Credentials].[usp_GetSyncDashboard]
AS
BEGIN
    SET NOCOUNT ON;
    
    -- Last sync
    PRINT 'LAST SYNC:'
    SELECT TOP 1 
        SyncID, SyncDateTime, SyncStatus,
        NewEntriesFound AS [New],
        ModifiedEntriesFound AS [Modified],
        DeletedEntriesFound AS [Deleted],
        DurationSeconds AS [Duration(s)]
    FROM [Credentials].[KeePassSyncHistory]
    ORDER BY SyncDateTime DESC
    
    -- Pending queue items
    PRINT ''
    PRINT 'PENDING QUEUE:'
    SELECT ChangeType, RequiresApproval, COUNT(*) AS Count
    FROM [Credentials].[KeePassSyncQueue]
    WHERE ProcessingStatus = 'Pending'
    GROUP BY ChangeType, RequiresApproval
    
    -- Snapshot stats
    PRINT ''
    PRINT 'SNAPSHOT STATS:'
    SELECT 
        COUNT(*) AS TotalInSnapshot,
        SUM(CASE WHEN IsActive = 1 THEN 1 ELSE 0 END) AS Active,
        MAX(LastSeenDateTime) AS LastSync
    FROM [Credentials].[KeePassCurrentSnapshot]
END

GO

-- [Credentials].[usp_ImportKeePassEntry]

-- ============================================================
-- Procedure: Import KeePass Entry to Staging
-- ============================================================
CREATE   PROCEDURE [Credentials].[usp_ImportKeePassEntry]
    @KeePassGroup NVARCHAR(500),
    @KeePassTitle NVARCHAR(500),
    @KeePassUsername NVARCHAR(255),
    @KeePassURL NVARCHAR(1000) = NULL,
    @KeePassNotes NVARCHAR(MAX) = NULL,
    @KeePassUUID NVARCHAR(100)
AS
BEGIN
    SET NOCOUNT ON;
    
    -- Check if already exists
    IF EXISTS (SELECT 1 FROM [Credentials].[KeePassMigrationStaging] WHERE KeePassUUID = @KeePassUUID)
    BEGIN
        PRINT 'Already exists: ' + @KeePassTitle
        RETURN 0
    END
    
    -- Auto-classify based on patterns
    DECLARE @AssignedType NVARCHAR(50)
    DECLARE @AssignedSensitivity NVARCHAR(50)
    
    SET @AssignedType = CASE 
        WHEN @KeePassGroup LIKE '%Service%' OR @KeePassTitle LIKE '%svc_%' THEN 'ServiceAccount'
        WHEN @KeePassGroup LIKE '%SQL%' OR @KeePassTitle LIKE '%sa%' THEN 'SQLLogin'
        WHEN @KeePassGroup LIKE '%Break%' OR @KeePassGroup LIKE '%Emergency%' THEN 'BreakGlass'
        WHEN @KeePassGroup LIKE '%Vendor%' THEN 'Vendor'
        WHEN @KeePassGroup LIKE '%Linked%' THEN 'LinkedServer'
        WHEN @KeePassGroup LIKE '%SSIS%' THEN 'SSIS'
        ELSE 'Application'
    END
    
    SET @AssignedSensitivity = CASE 
        WHEN @KeePassTitle LIKE '%sa%' OR @KeePassTitle LIKE '%admin%' OR @AssignedType = 'BreakGlass' THEN 'Critical'
        WHEN @AssignedType = 'ServiceAccount' THEN 'High'
        ELSE 'Medium'
    END
    
    INSERT INTO [Credentials].[KeePassMigrationStaging] (
        KeePassGroup, KeePassTitle, KeePassUsername, KeePassURL, KeePassNotes, KeePassUUID,
        AssignedCredentialType, AssignedSensitivity, ProcessingStatus
    )
    VALUES (
        @KeePassGroup, @KeePassTitle, @KeePassUsername, @KeePassURL, @KeePassNotes, @KeePassUUID,
        @AssignedType, @AssignedSensitivity, 'Pending'
    )
    
    PRINT 'Imported: ' + @KeePassTitle + ' (' + @AssignedType + ')'
    RETURN 1
END

GO

-- [Credentials].[usp_MigrateStagingToInventory]

-- ============================================================
-- Procedure: Migrate from Staging to Inventory
-- ============================================================
CREATE   PROCEDURE [Credentials].[usp_MigrateStagingToInventory]
    @StagingID INT,
    @VaultPath NVARCHAR(500),
    @VaultSecretName NVARCHAR(255),
    @VaultType NVARCHAR(100) = 'AzureKeyVault'
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @NewCredentialID INT
    DECLARE @CredentialTypeID INT
    DECLARE @VaultTypeID INT
    DECLARE @SensitivityID INT
    DECLARE @MigrationStatusID INT
    
    -- Get staging data
    DECLARE @Title NVARCHAR(500), @Username NVARCHAR(255), @Group NVARCHAR(500)
    DECLARE @UUID NVARCHAR(100), @Notes NVARCHAR(MAX)
    DECLARE @AssignedType NVARCHAR(50), @AssignedSensitivity NVARCHAR(50)
    
    SELECT 
        @Title = KeePassTitle,
        @Username = KeePassUsername,
        @Group = KeePassGroup,
        @UUID = KeePassUUID,
        @Notes = KeePassNotes,
        @AssignedType = ISNULL(AssignedCredentialType, 'Application'),
        @AssignedSensitivity = ISNULL(AssignedSensitivity, 'Medium')
    FROM [Credentials].[KeePassMigrationStaging]
    WHERE StagingID = @StagingID
    
    IF @Title IS NULL
    BEGIN
        RAISERROR('Staging record not found', 16, 1)
        RETURN -1
    END
    
    -- Get lookup IDs
    SELECT @CredentialTypeID = CredentialTypeID FROM [Credentials].[CredentialType] WHERE TypeName = @AssignedType
    SELECT @VaultTypeID = VaultTypeID FROM [Credentials].[VaultType] WHERE VaultName = @VaultType
    SELECT @SensitivityID = SensitivityID FROM [Credentials].[DataSensitivity] WHERE SensitivityLevel = @AssignedSensitivity
    SELECT @MigrationStatusID = StatusID FROM [Credentials].[MigrationStatus] WHERE StatusName = 'Migrated'
    
    -- Defaults
    IF @CredentialTypeID IS NULL SET @CredentialTypeID = 3  -- Application
    IF @VaultTypeID IS NULL SET @VaultTypeID = 1           -- AzureKeyVault
    IF @SensitivityID IS NULL SET @SensitivityID = 3       -- Medium
    IF @MigrationStatusID IS NULL SET @MigrationStatusID = 3 -- Migrated
    
    -- Get rotation days
    DECLARE @RotationDays INT
    SELECT @RotationDays = RotationFrequencyDays FROM [Credentials].[DataSensitivity] WHERE SensitivityID = @SensitivityID
    
    BEGIN TRY
        -- Insert to inventory
        INSERT INTO [Credentials].[CredentialInventory] (
            CredentialName, Username, CredentialTypeID, VaultTypeID, SensitivityID, MigrationStatusID,
            SourceSystem, SourceServer, KeePassGroup, KeePassEntryUUID,
            VaultPath, VaultSecretName,
            RotationFrequencyDays, NextRotationDate,
            MigrationDate, Notes
        )
        VALUES (
            @Title, @Username, @CredentialTypeID, @VaultTypeID, @SensitivityID, @MigrationStatusID,
            'KeePass', 'AZSQLADMIN19', @Group, @UUID,
            @VaultPath, @VaultSecretName,
            @RotationDays, DATEADD(DAY, @RotationDays, GETDATE()),
            GETDATE(), @Notes
        )
        
        SET @NewCredentialID = SCOPE_IDENTITY()
        
        -- Update staging
        UPDATE [Credentials].[KeePassMigrationStaging]
        SET ProcessingStatus = 'Migrated',
            ProcessedDate = GETDATE(),
            ProcessedBy = SUSER_SNAME(),
            MigratedCredentialID = @NewCredentialID
        WHERE StagingID = @StagingID
        
        PRINT 'Migrated: ' + @Title + ' (ID: ' + CAST(@NewCredentialID AS VARCHAR(10)) + ')'
        RETURN @NewCredentialID
        
    END TRY
    BEGIN CATCH
        DECLARE @Error NVARCHAR(4000) = ERROR_MESSAGE()
        UPDATE [Credentials].[KeePassMigrationStaging]
        SET ProcessingStatus = 'Error', ProcessingNotes = @Error
        WHERE StagingID = @StagingID
        
        RAISERROR('Migration failed: %s', 16, 1, @Error)
        RETURN -1
    END CATCH
END

GO

-- [Credentials].[usp_ProcessSyncQueue]

-- ============================================================
-- Procedure: Process Sync Queue (Auto-process non-sensitive)
-- ============================================================
CREATE   PROCEDURE [Credentials].[usp_ProcessSyncQueue]
    @AutoOnly BIT = 1
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @Processed INT = 0
    
    -- Process NEW entries that don't require approval
    DECLARE @QueueID BIGINT, @Title NVARCHAR(500), @Username NVARCHAR(255)
    DECLARE @URL NVARCHAR(1000), @Notes NVARCHAR(MAX), @ChangeType NVARCHAR(50)
    
    DECLARE queue_cursor CURSOR LOCAL FAST_FORWARD FOR
        SELECT QueueID, KeePassTitle, KeePassUsername, KeePassURL, KeePassNotes, ChangeType
        FROM [Credentials].[KeePassSyncQueue]
        WHERE ProcessingStatus = 'Pending'
          AND (@AutoOnly = 0 OR RequiresApproval = 0)
          AND ChangeType = 'New'
    
    OPEN queue_cursor
    FETCH NEXT FROM queue_cursor INTO @QueueID, @Title, @Username, @URL, @Notes, @ChangeType
    
    WHILE @@FETCH_STATUS = 0
    BEGIN
        -- Import to staging
        EXEC [Credentials].[usp_ImportKeePassEntry]
            @KeePassGroup = 'Synced',
            @KeePassTitle = @Title,
            @KeePassUsername = @Username,
            @KeePassURL = @URL,
            @KeePassNotes = @Notes,
            @KeePassUUID = @Title  -- Use title as UUID for synced entries
        
        -- Mark as processed
        UPDATE [Credentials].[KeePassSyncQueue]
        SET ProcessingStatus = 'Processed',
            ProcessedDateTime = GETDATE(),
            ProcessedBy = 'AutoSync'
        WHERE QueueID = @QueueID
        
        SET @Processed = @Processed + 1
        
        FETCH NEXT FROM queue_cursor INTO @QueueID, @Title, @Username, @URL, @Notes, @ChangeType
    END
    
    CLOSE queue_cursor
    DEALLOCATE queue_cursor
    
    SELECT @Processed AS ProcessedCount
END

GO

-- [Credentials].[usp_SearchCredentials]

-- Create the search procedure
CREATE   PROCEDURE Credentials.usp_SearchCredentials
    @SearchTerm NVARCHAR(100) = NULL,
    @CredentialType NVARCHAR(50) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    SELECT 
        ci.CredentialID,
        ci.CredentialName,
        ci.Username,
        ct.TypeName AS CredentialType,
        vt.VaultName AS VaultTarget,
        ci.Notes,
        ci.CreatedDate
    FROM Credentials.CredentialInventory ci
    JOIN Credentials.CredentialType ct ON ci.CredentialTypeID = ct.CredentialTypeID
    JOIN Credentials.VaultType vt ON ci.VaultTypeID = vt.VaultTypeID
    WHERE ci.IsActive = 1
      AND (@SearchTerm IS NULL 
           OR ci.CredentialName LIKE '%' + @SearchTerm + '%'
           OR ci.Username LIKE '%' + @SearchTerm + '%'
           OR ci.Notes LIKE '%' + @SearchTerm + '%')
      AND (@CredentialType IS NULL OR ct.TypeName = @CredentialType)
    ORDER BY ct.TypeName, ci.CredentialName
END

GO

-- [Credentials].[usp_SyncKeePassEntries]

-- ============================================================
-- Procedure: Sync KeePass Entries
-- ============================================================
CREATE   PROCEDURE [Credentials].[usp_SyncKeePassEntries]
    @SyncType NVARCHAR(50) = 'Scheduled'
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @SyncID BIGINT
    DECLARE @StartTime DATETIME2 = GETDATE()
    DECLARE @NewCount INT = 0
    DECLARE @ModifiedCount INT = 0
    DECLARE @DeletedCount INT = 0
    DECLARE @TotalSource INT = 0
    
    -- Create sync history record
    INSERT INTO [Credentials].[KeePassSyncHistory] (SyncType, StartTime, SyncStatus)
    VALUES (@SyncType, @StartTime, 'InProgress')
    SET @SyncID = SCOPE_IDENTITY()
    
    -- Count source entries
    SELECT @TotalSource = COUNT(*) FROM #KeePassImport
    
    -- ========== DETECT NEW ENTRIES ==========
    INSERT INTO [Credentials].[KeePassSyncQueue] (
        KeePassTitle, KeePassUsername, KeePassURL, KeePassNotes,
        ChangeType, RequiresApproval
    )
    SELECT 
        i.KeePassTitle, i.KeePassUsername, i.KeePassURL, i.KeePassNotes,
        'New',
        CASE WHEN i.KeePassTitle LIKE '%admin%' OR i.KeePassTitle LIKE '%sa%' THEN 1 ELSE 0 END
    FROM #KeePassImport i
    LEFT JOIN [Credentials].[KeePassCurrentSnapshot] s 
        ON i.KeePassTitle = s.KeePassTitle AND i.KeePassUsername = s.KeePassUsername
    WHERE s.SnapshotID IS NULL
    
    SET @NewCount = @@ROWCOUNT
    
    -- ========== DETECT MODIFIED ENTRIES ==========
    INSERT INTO [Credentials].[KeePassSyncQueue] (
        KeePassTitle, KeePassUsername, KeePassURL, KeePassNotes,
        ChangeType, ExistingCredentialID, RequiresApproval
    )
    SELECT 
        i.KeePassTitle, i.KeePassUsername, i.KeePassURL, i.KeePassNotes,
        'Modified',
        s.LinkedCredentialID,
        CASE WHEN i.KeePassUsername != s.KeePassUsername THEN 1 ELSE 0 END
    FROM #KeePassImport i
    INNER JOIN [Credentials].[KeePassCurrentSnapshot] s 
        ON i.KeePassTitle = s.KeePassTitle
    WHERE s.ContentHash != HASHBYTES('SHA2_256', 
        CONCAT(ISNULL(i.KeePassTitle,''), '|', ISNULL(i.KeePassUsername,''), '|', ISNULL(i.KeePassURL,''), '|', ISNULL(i.KeePassNotes,'')))
    
    SET @ModifiedCount = @@ROWCOUNT
    
    -- ========== DETECT DELETED ENTRIES ==========
    INSERT INTO [Credentials].[KeePassSyncQueue] (
        KeePassTitle, KeePassUsername,
        ChangeType, ExistingCredentialID, RequiresApproval
    )
    SELECT 
        s.KeePassTitle, s.KeePassUsername,
        'Deleted',
        s.LinkedCredentialID,
        1  -- Deletions always require approval
    FROM [Credentials].[KeePassCurrentSnapshot] s
    LEFT JOIN #KeePassImport i ON s.KeePassTitle = i.KeePassTitle
    WHERE i.KeePassTitle IS NULL AND s.IsActive = 1
    
    SET @DeletedCount = @@ROWCOUNT
    
    -- ========== UPDATE SNAPSHOT ==========
    -- Update existing
    UPDATE s
    SET KeePassUsername = i.KeePassUsername,
        KeePassURL = i.KeePassURL,
        KeePassNotes = i.KeePassNotes,
        LastSeenDateTime = GETDATE()
    FROM [Credentials].[KeePassCurrentSnapshot] s
    INNER JOIN #KeePassImport i ON s.KeePassTitle = i.KeePassTitle
    
    -- Insert new
    INSERT INTO [Credentials].[KeePassCurrentSnapshot] (
        KeePassTitle, KeePassUsername, KeePassURL, KeePassNotes
    )
    SELECT i.KeePassTitle, i.KeePassUsername, i.KeePassURL, i.KeePassNotes
    FROM #KeePassImport i
    LEFT JOIN [Credentials].[KeePassCurrentSnapshot] s ON i.KeePassTitle = s.KeePassTitle
    WHERE s.SnapshotID IS NULL
    
    -- ========== UPDATE SYNC HISTORY ==========
    UPDATE [Credentials].[KeePassSyncHistory]
    SET TotalEntriesInSource = @TotalSource,
        NewEntriesFound = @NewCount,
        ModifiedEntriesFound = @ModifiedCount,
        DeletedEntriesFound = @DeletedCount,
        EndTime = GETDATE(),
        SyncStatus = 'Success'
    WHERE SyncID = @SyncID
    
    -- Return summary
    SELECT 
        @SyncID AS SyncID,
        @TotalSource AS TotalEntries,
        @NewCount AS NewEntries,
        @ModifiedCount AS ModifiedEntries,
        @DeletedCount AS DeletedEntries
END

GO

-- [ctl].[usp_CaptureServerInventoryHistory]
CREATE OR ALTER PROCEDURE ctl.usp_CaptureServerInventoryHistory
AS
BEGIN
    SET NOCOUNT ON;

    INSERT INTO fact.ServerInventoryHistory
    (
        ServerName,
        SQLVersion,
        CPUCount,
        TotalMemoryMB,
        MaxMemoryMB,
        MaxDOP,
        CostThreshold,
        SnapshotDate
    )
    SELECT 
        ServerName,
        SQLVersion,
        CPUCount,
        TotalMemoryMB,
        MaxMemoryMB,
        MaxDOP,
        CostThreshold,
        GETDATE()
    FROM config.ServerInventory;
END

GO

-- [ctl].[usp_Check_ReplicationHealth]

CREATE PROCEDURE ctl.usp_Check_ReplicationHealth
    @Verbose BIT = 0,
    @LatencyThreshold INT = 600,
    @PendingCmdsThreshold INT = 600
AS
BEGIN
    SET NOCOUNT ON;
    
    -- First collect data
    EXEC ctl.usp_Collect_ReplicationHealth @Verbose = @Verbose;
    
    -- Check for issues (threshold violations would go here)
    -- For now, just report status
    IF @Verbose = 1
    BEGIN
        SELECT 
            PublisherName,
            COUNT(*) AS PublicationCount,
            MAX(CollectionTime) AS LastCollection
        FROM fact.ReplicationHealth
        WHERE CollectionTime >= DATEADD(MINUTE, -5, GETDATE())
        GROUP BY PublisherName;
    END
    
    PRINT 'Health Check Complete - Thresholds: Latency=' + CAST(@LatencyThreshold AS VARCHAR) + 's, PendingCmds=' + CAST(@PendingCmdsThreshold AS VARCHAR);
END

GO

-- [ctl].[usp_Collect_AllMetrics]

CREATE PROCEDURE ctl.usp_Collect_AllMetrics
AS
BEGIN
    SET NOCOUNT ON;
    
    PRINT 'Starting DBAOps Master Collection - ' + CONVERT(VARCHAR, GETDATE(), 120);
    
    -- Collect Audit Events
    BEGIN TRY
        IF OBJECT_ID('audit.usp_Collect_LoginFailures', 'P') IS NOT NULL
            EXEC audit.usp_Collect_LoginFailures;
        IF OBJECT_ID('audit.usp_Collect_PermissionChanges', 'P') IS NOT NULL
            EXEC audit.usp_Collect_PermissionChanges;
        IF OBJECT_ID('audit.usp_Collect_ObjectChanges', 'P') IS NOT NULL
            EXEC audit.usp_Collect_ObjectChanges;
        PRINT '  Audit collection complete';
    END TRY
    BEGIN CATCH
        PRINT '  Audit collection error: ' + ERROR_MESSAGE();
    END CATCH
    
    -- Collect Server Inventory
    BEGIN TRY
        IF OBJECT_ID('ctl.usp_Collect_ServerInventory', 'P') IS NOT NULL
            EXEC ctl.usp_Collect_ServerInventory;
        PRINT '  Server inventory collection complete';
    END TRY
    BEGIN CATCH
        PRINT '  Server inventory error: ' + ERROR_MESSAGE();
    END CATCH
    
    PRINT 'Master Collection Complete - ' + CONVERT(VARCHAR, GETDATE(), 120);
END

GO

-- [ctl].[usp_Collect_DiskSpace]

CREATE PROCEDURE ctl.usp_Collect_DiskSpace
AS
BEGIN
    SET NOCOUNT ON;

    INSERT INTO fact.DiskSpaceMetrics
    (
        ServerName,
        DriveLetter,
        VolumeLabel,
        TotalSpaceGB,
        FreeSpaceGB,
        FreePercent,
        CaptureDate
    )
    SELECT  
        -- Explicitly convert the sql_variant output to nvarchar(128)
        CONVERT(nvarchar(128), SERVERPROPERTY('MachineName')) AS ServerName,
        LEFT(CONVERT(nvarchar(260), vs.volume_mount_point), 1) AS DriveLetter,
        CONVERT(nvarchar(128), vs.logical_volume_name) AS VolumeLabel,
        vs.total_bytes / 1024.0 / 1024 / 1024 AS TotalSpaceGB,
        vs.available_bytes / 1024.0 / 1024 / 1024 AS FreeSpaceGB,
        (vs.available_bytes * 100.0 / vs.total_bytes) AS FreePercent,
        GETDATE() AS CaptureDate
    FROM sys.master_files mf
    CROSS APPLY sys.dm_os_volume_stats(mf.database_id, mf.file_id) vs
    GROUP BY 
        vs.volume_mount_point, 
        vs.logical_volume_name, 
        vs.total_bytes, 
        vs.available_bytes;
END

GO

-- [ctl].[usp_Collect_DiskSpaceCompliance]

CREATE PROCEDURE ctl.usp_Collect_DiskSpaceCompliance
AS
BEGIN
    SET NOCOUNT ON;

    -- [Paste your organization's CREATE PROCEDURE code here]
    -- Example placeholder logic for gathering basic disk space info using system views:
    SELECT 
        dovs.volume_mount_point AS Drive, 
        CAST(dovs.available_bytes / 1048576.0 AS DECIMAL(15, 2)) AS FreeSpaceInMB,
        CAST(dovs.total_bytes / 1048576.0 AS DECIMAL(15, 2)) AS TotalSpaceInMB,
        CAST(dovs.available_bytes * 100.0 / dovs.total_bytes AS DECIMAL(5, 2)) AS PercentFree
    FROM sys.master_files AS mf
    CROSS APPLY sys.dm_os_volume_stats(mf.database_id, mf.file_id) AS dovs
    GROUP BY dovs.volume_mount_point, dovs.available_bytes, dovs.total_bytes
    ORDER BY Drive;

END;

GO

-- [ctl].[usp_Collect_JobCompliance]

-- ============================================================================
-- STEP 2: Fix the stored procedure
-- ============================================================================
CREATE PROCEDURE ctl.usp_Collect_JobCompliance  
AS  
BEGIN  
    SET NOCOUNT ON;
    
    DECLARE @Now DATETIME = GETDATE();
    DECLARE @RowsInserted INT = 0;
    DECLARE @NonCompliantCount INT = 0;
    
    BEGIN TRY
        -- ====================================================================
        -- Clear today's data to avoid duplicates (idempotent)
        -- ====================================================================
        DELETE FROM ctl.JobCompliance 
        WHERE ServerName = @@SERVERNAME 
          AND CAST(LastRunDateTime AS DATE) = CAST(@Now AS DATE);
        
        -- ====================================================================
        -- Insert job compliance data with proper date conversion
        -- ====================================================================
        INSERT INTO ctl.JobCompliance (
            ServerName, 
            JobName, 
            LastRunDateTime, 
            LastRunStatus, 
            ExpectedFrequency, 
            IsCompliant
        )
        SELECT   
            @@SERVERNAME AS ServerName,
            j.name AS JobName,
            -- Use agent_datetime function for safe conversion
            CASE 
                WHEN h.run_date IS NULL THEN NULL
                ELSE msdb.dbo.agent_datetime(h.run_date, h.run_time)
            END AS LastRunDateTime,
            CASE h.run_status 
                WHEN 1 THEN 'Succeeded' 
                WHEN 0 THEN 'Failed'
                WHEN 2 THEN 'Retry'
                WHEN 3 THEN 'Canceled'
                ELSE 'Unknown'
            END AS LastRunStatus,
            'Daily' AS ExpectedFrequency,
            CASE 
                WHEN h.run_status = 1 THEN 1 
                ELSE 0 
            END AS IsCompliant
        FROM msdb.dbo.sysjobs j  
        LEFT JOIN (
            -- Get only the latest run for each job
            SELECT job_id, MAX(instance_id) AS max_instance
            FROM msdb.dbo.sysjobhistory
            WHERE step_id = 0  -- Job outcome only
            GROUP BY job_id
        ) latest ON j.job_id = latest.job_id
        LEFT JOIN msdb.dbo.sysjobhistory h ON latest.job_id = h.job_id  
            AND latest.max_instance = h.instance_id
        WHERE j.enabled = 1
          AND j.name NOT LIKE 'syspolicy%';
        
        SET @RowsInserted = @@ROWCOUNT;
        
        -- Count non-compliant jobs
        SELECT @NonCompliantCount = COUNT(*)
        FROM ctl.JobCompliance
        WHERE ServerName = @@SERVERNAME
          AND CAST(LastRunDateTime AS DATE) = CAST(@Now AS DATE)
          AND IsCompliant = 0;
        
        PRINT CONCAT('Job compliance collected: ', @RowsInserted, ' jobs, ', @NonCompliantCount, ' non-compliant');
        
    END TRY
    BEGIN CATCH
        PRINT CONCAT('ERROR: ', ERROR_MESSAGE());
        THROW;
    END CATCH
END;

GO

-- [ctl].[usp_Collect_LoginCompliance]
CREATE OR ALTER PROCEDURE ctl.usp_Collect_LoginCompliance
AS
BEGIN
    INSERT INTO ctl.LoginCompliance (ServerName, LoginName, Status, MustChange, IsDisabled)
    SELECT 
        @@SERVERNAME,
        name,
        type_desc,
        is_expiration_checked,
        is_disabled
    FROM sys.sql_logins;
END;

GO

-- [ctl].[usp_Collect_MemoryCompliance]

CREATE   PROCEDURE ctl.usp_Collect_MemoryCompliance
AS
BEGIN
    SET NOCOUNT ON;
    
    SELECT 
        cms.ServerName,
        COALESCE(cms.Environment, cms.CMSEnvironment, 'Unknown') AS Environment,
        cms.TotalMemoryGB,
        cms.CPUCount,
        cms.ComplianceTier
    FROM config.ServerInventory cms
    WHERE cms.IsActive = 1
    AND (cms.ServerType = 'SQL Server' OR cms.ServerType IS NULL)
    ORDER BY cms.Environment, cms.ServerName;
END

GO

-- [ctl].[usp_Collect_PerformanceBaseline]
CREATE OR ALTER PROCEDURE ctl.usp_Collect_PerformanceBaseline
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE 
        @CPUPercent      FLOAT,
        @MemoryUsedMB    FLOAT,
        @PLE             FLOAT;

    ------------------------------------------------------------
    -- CPU %
    ------------------------------------------------------------
    SELECT TOP 1 
        @CPUPercent =
        CAST(
            (CONVERT(XML, rb.record).value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]', 'int'))
            /
            NULLIF(CONVERT(XML, rb.record).value('(./Record/SchedulerMonitorEvent/SystemHealth/ProcessUtilization)[1]', 'int'),0)
        AS float)
    FROM sys.dm_os_ring_buffers rb
    WHERE rb.ring_buffer_type = 'RING_BUFFER_SCHEDULER_MONITOR'
    ORDER BY rb.timestamp DESC;

    ------------------------------------------------------------
    -- Memory Used MB
    ------------------------------------------------------------
    SELECT @MemoryUsedMB = (total_physical_memory_kb - available_physical_memory_kb) / 1024
    FROM sys.dm_os_sys_memory;

    ------------------------------------------------------------
    -- Page Life Expectancy
    ------------------------------------------------------------
    SELECT @PLE = cntr_value
    FROM sys.dm_os_performance_counters
    WHERE counter_name = 'Page life expectancy'
      AND object_name LIKE '%Buffer Manager%';

    ------------------------------------------------------------
    -- Insert Wait Stats with Metrics
    ------------------------------------------------------------
    INSERT INTO metrics.PerformanceBaseline
    (
        ServerName,
        CPUPercent,
        MemoryUsedMB,
        PLE,
        WaitCategory,
        WaitSeconds,
        CaptureTime
    )
    SELECT
        @@SERVERNAME,
        @CPUPercent,
        @MemoryUsedMB,
        @PLE,
        ws.WaitCategory,
        ws.WaitSeconds,
        GETDATE()
    FROM dbo.Stg_WaitStats ws;
END

GO

-- [ctl].[usp_Collect_PerformanceCompliance]

CREATE PROCEDURE ctl.usp_Collect_PerformanceCompliance
AS
BEGIN
    SET NOCOUNT ON;

    ---------------------------------------------------------
    -- Top 3 Waits (Safe version, no OFFSET/FETCH issues)
    ---------------------------------------------------------
    ;WITH TopWaits AS
    (
        SELECT TOP (3)
            wait_type,
            ROW_NUMBER() OVER (ORDER BY wait_time_ms DESC) AS rn
        FROM sys.dm_os_wait_stats
        WHERE wait_type NOT LIKE '%SLEEP%'
          AND wait_type NOT LIKE 'SQLTRACE%'
        ORDER BY wait_time_ms DESC
    )
    INSERT INTO dbo.PerformanceComplianceInventory
    (
        ServerName,
        SQLVersion,
        Edition,
        CpuCount,
        MaxServerMemoryGB,
        TotalMemoryGB,
        TempDBFiles,
        TempDBDataSizeMB,
        TempDBLogSizeMB,
        PageLifeExpectancy,
        SignalWaitPercent,
        WaitsTop1,
        WaitsTop2,
        WaitsTop3,
        CollectionTime
    )
    SELECT
        @@SERVERNAME,
        CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(50)),
        CAST(SERVERPROPERTY('Edition') AS NVARCHAR(100)),
        info.cpu_count,

        -- Safe conversion from sql_variant
        (
            SELECT CONVERT(DECIMAL(10,2), value_in_use) / 1024.0
            FROM sys.configurations
            WHERE name = 'max server memory (MB)'
        ) AS MaxServerMemoryGB,

        -- Total physical memory GB
        CONVERT(DECIMAL(10,2),
            mem.total_physical_memory_kb/1024.0/1024.0
        ) AS TotalMemoryGB,

        -- TempDB Files
        (SELECT COUNT(*) FROM tempdb.sys.database_files WHERE type = 0),

        -- TempDB Data size MB
        (SELECT SUM(size)/128 FROM tempdb.sys.database_files WHERE type = 0),

        -- TempDB Log size MB
        (SELECT SUM(size)/128 FROM tempdb.sys.database_files WHERE type = 1),

        -- PLE
        (
            SELECT cntr_value
            FROM sys.dm_os_performance_counters
            WHERE counter_name = 'Page life expectancy'
              AND object_name LIKE '%Buffer Manager%'
        ) AS PageLifeExpectancy,

        -- Signal Wait %
        CONVERT(DECIMAL(10,2),
            100.0 *
            (SELECT SUM(signal_wait_time_ms) FROM sys.dm_os_wait_stats) /
            NULLIF((SELECT SUM(wait_time_ms) FROM sys.dm_os_wait_stats),0)
        ) AS SignalWaitPercent,

        -- Top 3 waits
        (SELECT wait_type FROM TopWaits WHERE rn = 1),
        (SELECT wait_type FROM TopWaits WHERE rn = 2),
        (SELECT wait_type FROM TopWaits WHERE rn = 3),

        GETDATE()
    FROM sys.dm_os_sys_info info
    CROSS JOIN sys.dm_os_sys_memory mem;
END

GO

-- [ctl].[usp_Collect_PerformanceCompliance_AllServers]

CREATE   PROCEDURE ctl.usp_Collect_PerformanceCompliance_AllServers
AS
BEGIN
    SET NOCOUNT ON;
    
    SELECT 
        cms.ServerName,
        COALESCE(cms.Environment, cms.CMSEnvironment, 'Unknown') AS Environment,
        cms.SQLVersion,
        cms.MaxDOP,
        cms.CostThreshold,
        cms.TotalMemoryGB
    FROM config.ServerInventory cms
    WHERE cms.IsActive = 1
    AND (cms.ServerType = 'SQL Server' OR cms.ServerType IS NULL)
    ORDER BY cms.Environment, cms.ServerName;
END

GO

-- [ctl].[usp_Collect_PerformanceCompliance_Remote]

CREATE   PROCEDURE ctl.usp_Collect_PerformanceCompliance_Remote
AS
BEGIN
    SET NOCOUNT ON;

    ;WITH TopWaits AS
    (
        SELECT TOP (3)
            wait_type,
            ROW_NUMBER() OVER (ORDER BY wait_time_ms DESC) AS rn
        FROM sys.dm_os_wait_stats
        WHERE wait_type NOT LIKE '%SLEEP%'
          AND wait_type NOT LIKE 'SQLTRACE%'
        ORDER BY wait_time_ms DESC
    )
    SELECT
        @@SERVERNAME AS ServerName,
        CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(50)) AS SQLVersion,
        CAST(SERVERPROPERTY('Edition') AS NVARCHAR(100)) AS Edition,
        info.cpu_count AS CpuCount,

        -- Max memory
        (SELECT CONVERT(DECIMAL(10,2), value_in_use) / 1024.0
         FROM sys.configurations
         WHERE name = 'max server memory (MB)') AS MaxServerMemoryGB,

        CONVERT(DECIMAL(10,2), mem.total_physical_memory_kb / 1024.0 / 1024.0) AS TotalMemoryGB,

        (SELECT COUNT(*) FROM tempdb.sys.database_files WHERE type = 0) AS TempDBFiles,
        (SELECT SUM(size)/128 FROM tempdb.sys.database_files WHERE type = 0) AS TempDBDataSizeMB,
        (SELECT SUM(size)/128 FROM tempdb.sys.database_files WHERE type = 1) AS TempDBLogSizeMB,

        (SELECT cntr_value
         FROM sys.dm_os_performance_counters
         WHERE counter_name='Page life expectancy'
           AND object_name LIKE '%Buffer Manager%') AS PageLifeExpectancy,

        CONVERT(DECIMAL(10,2),
            100.0 *
            (SELECT SUM(signal_wait_time_ms) FROM sys.dm_os_wait_stats) /
            NULLIF((SELECT SUM(wait_time_ms) FROM sys.dm_os_wait_stats),0)
        ) AS SignalWaitPercent,

        (SELECT wait_type FROM TopWaits WHERE rn=1) AS WaitsTop1,
        (SELECT wait_type FROM TopWaits WHERE rn=2) AS WaitsTop2,
        (SELECT wait_type FROM TopWaits WHERE rn=3) AS WaitsTop3,

        GETDATE() AS CollectionTime
    FROM sys.dm_os_sys_info info
    CROSS JOIN sys.dm_os_sys_memory mem;
END

GO

-- [ctl].[usp_Collect_PermissionChanges]

CREATE PROCEDURE ctl.usp_Collect_PermissionChanges
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @TraceFile NVARCHAR(500);
    DECLARE @LastCollection DATETIME;
    DECLARE @RowsInserted INT = 0;
    
    BEGIN TRY
        -- Get default trace file path
        SELECT @TraceFile = path FROM sys.traces WHERE is_default = 1;
        
        IF @TraceFile IS NULL
        BEGIN
            PRINT 'Default trace not enabled';
            RETURN;
        END
        
        -- Get last collection time
        SELECT @LastCollection = ISNULL(MAX(EventTime), DATEADD(DAY, -1, GETDATE())) 
        FROM audit.PermissionChanges;
        
        -- Insert permission events from default trace
        INSERT INTO audit.PermissionChanges (
            ServerName, 
            Permission,
            ChangeTime,
            UserName,
            ChangedBy,
            DatabaseName, 
            EventTime,
            ObjectName, 
            ChangeType, 
            CommandText
        )
        SELECT DISTINCT
            @@SERVERNAME,
            ISNULL(t.ObjectName, 'N/A'),           -- Permission
            t.StartTime,                            -- ChangeTime
            ISNULL(t.TargetLoginName, 'N/A'),      -- UserName (who was affected)
            ISNULL(t.LoginName, 'Unknown'),        -- ChangedBy (who made change)
            ISNULL(t.DatabaseName, 'N/A'),         -- DatabaseName
            t.StartTime,                            -- EventTime
            ISNULL(t.ObjectName, 'N/A'),           -- ObjectName
            CASE t.EventClass
                WHEN 102 THEN 'Database GDR'
                WHEN 103 THEN 'Object GDR'
                WHEN 104 THEN 'Add Login'
                WHEN 105 THEN 'Login GDR'
                WHEN 106 THEN 'Login Property Change'
                WHEN 108 THEN 'Add to Server Role'
                WHEN 109 THEN 'Add DB User'
                WHEN 110 THEN 'Add to DB Role'
                WHEN 111 THEN 'Add Role'
                ELSE 'Permission Change'
            END,                                    -- ChangeType
            LEFT(ISNULL(CAST(t.TextData AS NVARCHAR(MAX)), ''), 4000)  -- CommandText
        FROM fn_trace_gettable(@TraceFile, DEFAULT) t
        WHERE t.EventClass IN (102, 103, 104, 105, 106, 108, 109, 110, 111)
        AND t.StartTime > @LastCollection
        AND t.StartTime IS NOT NULL;
        
        SET @RowsInserted = @@ROWCOUNT;
        PRINT 'Collected ' + CAST(@RowsInserted AS VARCHAR) + ' permission change events';
        
    END TRY
    BEGIN CATCH
        PRINT 'Error: ' + ERROR_MESSAGE();
    END CATCH
END

GO

-- [ctl].[usp_Collect_ReplicationHealth]

CREATE PROCEDURE ctl.usp_Collect_ReplicationHealth
    @Verbose BIT = 0,
    @LatencyThreshold INT = 600,
    @PendingCmdsThreshold INT = 600
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @CollectionTime DATETIME = GETDATE();
    DECLARE @TotalRows INT = 0;
    DECLARE @RowCount INT;
    
    IF @Verbose = 1
        PRINT 'Starting Replication Health Collection - ' + CONVERT(VARCHAR, @CollectionTime, 120);
    
    -- AZAPPPRODSQL19
    BEGIN TRY
        INSERT INTO fact.ReplicationHealth 
            (PublisherName, PublisherDB, PublicationName, LatencySeconds, PendingCommands, Status, CollectionTime)
        SELECT 'AZAPPPRODSQL19', publisher_db, publication, 0, 0, 'Active', @CollectionTime
        FROM [AZAPPPRODSQL19].distribution.dbo.MSpublications;
        SET @RowCount = @@ROWCOUNT;
        SET @TotalRows = @TotalRows + @RowCount;
        IF @Verbose = 1 PRINT '  AZAPPPRODSQL19: ' + CAST(@RowCount AS VARCHAR) + ' publications';
    END TRY
    BEGIN CATCH
        IF @Verbose = 1 PRINT '  AZAPPPRODSQL19 ERROR: ' + ERROR_MESSAGE();
    END CATCH
    
    -- AZSAFESSQL01
    BEGIN TRY
        INSERT INTO fact.ReplicationHealth 
            (PublisherName, PublisherDB, PublicationName, LatencySeconds, PendingCommands, Status, CollectionTime)
        SELECT 'AZSAFESSQL01', publisher_db, publication, 0, 0, 'Active', @CollectionTime
        FROM [AZSAFESSQL01].distribution.dbo.MSpublications;
        SET @RowCount = @@ROWCOUNT;
        SET @TotalRows = @TotalRows + @RowCount;
        IF @Verbose = 1 PRINT '  AZSAFESSQL01: ' + CAST(@RowCount AS VARCHAR) + ' publications';
    END TRY
    BEGIN CATCH
        IF @Verbose = 1 PRINT '  AZSAFESSQL01 ERROR: ' + ERROR_MESSAGE();
    END CATCH
    
    -- AZSPILLMANSQL19
    BEGIN TRY
        INSERT INTO fact.ReplicationHealth 
            (PublisherName, PublisherDB, PublicationName, LatencySeconds, PendingCommands, Status, CollectionTime)
        SELECT 'AZSPILLMANSQL19', publisher_db, publication, 0, 0, 'Active', @CollectionTime
        FROM [AZSPILLMANSQL19].distribution.dbo.MSpublications;
        SET @RowCount = @@ROWCOUNT;
        SET @TotalRows = @TotalRows + @RowCount;
        IF @Verbose = 1 PRINT '  AZSPILLMANSQL19: ' + CAST(@RowCount AS VARCHAR) + ' publications';
    END TRY
    BEGIN CATCH
        IF @Verbose = 1 PRINT '  AZSPILLMANSQL19 ERROR: ' + ERROR_MESSAGE();
    END CATCH
    
    -- AZTRACKITSQL01
    BEGIN TRY
        INSERT INTO fact.ReplicationHealth 
            (PublisherName, PublisherDB, PublicationName, LatencySeconds, PendingCommands, Status, CollectionTime)
        SELECT 'AZTRACKITSQL01', publisher_db, publication, 0, 0, 'Active', @CollectionTime
        FROM [AZTRACKITSQL01].distribution.dbo.MSpublications;
        SET @RowCount = @@ROWCOUNT;
        SET @TotalRows = @TotalRows + @RowCount;
        IF @Verbose = 1 PRINT '  AZTRACKITSQL01: ' + CAST(@RowCount AS VARCHAR) + ' publications';
    END TRY
    BEGIN CATCH
        IF @Verbose = 1 PRINT '  AZTRACKITSQL01 ERROR: ' + ERROR_MESSAGE();
    END CATCH
    
    -- AZTRACSSQL16
    BEGIN TRY
        INSERT INTO fact.ReplicationHealth 
            (PublisherName, PublisherDB, PublicationName, LatencySeconds, PendingCommands, Status, CollectionTime)
        SELECT 'AZTRACSSQL16', publisher_db, publication, 0, 0, 'Active', @CollectionTime
        FROM [AZTRACSSQL16].distribution.dbo.MSpublications;
        SET @RowCount = @@ROWCOUNT;
        SET @TotalRows = @TotalRows + @RowCount;
        IF @Verbose = 1 PRINT '  AZTRACSSQL16: ' + CAST(@RowCount AS VARCHAR) + ' publications';
    END TRY
    BEGIN CATCH
        IF @Verbose = 1 PRINT '  AZTRACSSQL16 ERROR: ' + ERROR_MESSAGE();
    END CATCH
    
    -- Update LastChecked
    UPDATE config.ReplicationPublishers SET LastChecked = @CollectionTime WHERE IsActive = 1;
    
    IF @Verbose = 1
        PRINT 'Collection Complete - Total: ' + CAST(@TotalRows AS VARCHAR) + ' publications';
    
    SELECT @TotalRows AS RowsCollected;
END

GO

-- [ctl].[usp_Collect_ReplicationMetrics]

CREATE PROCEDURE ctl.usp_Collect_ReplicationMetrics
    @PublisherServer NVARCHAR(128) = NULL,
    @Verbose BIT = 0
AS
BEGIN
    /*
    â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
    DBAOps Monitoring and Compliance Framework
    Procedure: ctl.usp_Collect_ReplicationMetrics
    Author:    John Bosco RHOYLLE INNOVATION EMPIRE™, Sr SQL DBA/DBAOps Engineer, Microsoft Certified
    â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
    */
    SET NOCOUNT ON;
    
    DECLARE @CurrentPublisher NVARCHAR(128);
    
    IF @Verbose = 1
        PRINT 'Starting replication metrics collection at ' + CONVERT(VARCHAR, GETDATE(), 120);
    
    IF @PublisherServer IS NULL
        TRUNCATE TABLE ctl.ReplicationMetrics;
    ELSE
        DELETE FROM ctl.ReplicationMetrics WHERE PublisherServer = @PublisherServer;
    
    DECLARE publisher_cursor CURSOR FOR
        SELECT DISTINCT PublisherServer 
        FROM config.ReplicationServers 
        WHERE IsActive = 1
          AND (@PublisherServer IS NULL OR PublisherServer = @PublisherServer);
    
    OPEN publisher_cursor;
    FETCH NEXT FROM publisher_cursor INTO @CurrentPublisher;
    
    WHILE @@FETCH_STATUS = 0
    BEGIN
        IF @Verbose = 1
            PRINT '  Collecting from: ' + @CurrentPublisher;
        
        BEGIN TRY
            INSERT INTO ctl.ReplicationMetrics (
                PublisherServer, PendingCmdCount, LatencySeconds, AgentStatus,
                Status, StatusMessage, CollectionTime
            )
            SELECT 
                @CurrentPublisher, 0, 0, 'Running',
                CASE 
                    WHEN c.IsCompliant = 1 AND c.Status = 'Success' THEN 'Healthy'
                    WHEN c.IsCompliant = 1 THEN 'Warning'
                    ELSE 'Unknown'
                END,
                'Server online. Last backup: ' + CONVERT(VARCHAR, c.LastBackupTime, 120),
                GETDATE()
            FROM ctl.BackupCompliance c
            WHERE c.ServerName = @CurrentPublisher;
            
            IF @@ROWCOUNT = 0
            BEGIN
                INSERT INTO ctl.ReplicationMetrics (PublisherServer, Status, StatusMessage, CollectionTime)
                VALUES (@CurrentPublisher, 'Unknown', 'No backup compliance record found', GETDATE());
            END
        END TRY
        BEGIN CATCH
            INSERT INTO ctl.ReplicationMetrics (PublisherServer, Status, StatusMessage, CollectionTime)
            VALUES (@CurrentPublisher, 'Error', 'Collection failed: ' + ERROR_MESSAGE(), GETDATE());
        END CATCH
        
        FETCH NEXT FROM publisher_cursor INTO @CurrentPublisher;
    END
    
    CLOSE publisher_cursor;
    DEALLOCATE publisher_cursor;
    
    -- Archive to history
    INSERT INTO fact.ReplicationMetricsHistory (
        PublisherServer, PublisherDatabase, PublicationName, SubscriberServer, SubscriberDatabase,
        PendingCmdCount, LatencySeconds, DeliveryRate, Status, CollectionTime
    )
    SELECT 
        PublisherServer, PublisherDatabase, PublicationName, SubscriberServer, SubscriberDatabase,
        PendingCmdCount, LatencySeconds, DeliveryRate, Status, CollectionTime
    FROM ctl.ReplicationMetrics;
    
    IF @Verbose = 1
        PRINT 'Collection complete at ' + CONVERT(VARCHAR, GETDATE(), 120);
    
    SELECT 
        PublisherServer,
        ISNULL(PendingCmdCount, 0) AS PendingCmds,
        ISNULL(LatencySeconds, 0) AS LatencySec,
        ISNULL(AgentStatus, 'Unknown') AS AgentStatus,
        Status,
        StatusMessage,
        CollectionTime
    FROM ctl.ReplicationMetrics
    ORDER BY 
        CASE Status WHEN 'Critical' THEN 1 WHEN 'Warning' THEN 2 WHEN 'Unknown' THEN 3 ELSE 4 END,
        PublisherServer;
END

GO

-- [ctl].[usp_Collect_ReplicationMetrics_LinkedServer]

CREATE PROCEDURE ctl.usp_Collect_ReplicationMetrics_LinkedServer
    @Verbose BIT = 0
AS
BEGIN
    /*
    â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
    DBAOps Monitoring and Compliance Framework
    Procedure: ctl.usp_Collect_ReplicationMetrics_LinkedServer
    Author:    John Bosco RHOYLLE INNOVATION EMPIRE™, Sr SQL DBA/DBAOps Engineer, Microsoft Certified
    
    Purpose:   Collects replication metrics from each publisher via linked server
    Prerequisites:
      - Linked servers configured to each publisher
      - usp_Get_ReplicationMetrics deployed to each publisher's distribution DB
    â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
    */
    SET NOCOUNT ON;
    
    DECLARE @Publisher NVARCHAR(128);
    DECLARE @SQL NVARCHAR(MAX);
    
    TRUNCATE TABLE ctl.ReplicationMetrics;
    
    IF @Verbose = 1
        PRINT 'Starting linked server collection at ' + CONVERT(VARCHAR, GETDATE(), 120);
    
    DECLARE @Publishers TABLE (PublisherServer NVARCHAR(128));
    INSERT @Publishers VALUES 
        ('AZSAFESSQL01'), ('AZSPILLMANSQL19'), ('AZAPPPRODSQL19'),
        ('AZTRACKITSQL01'), ('AZTRACSSQL16');
    
    DECLARE publisher_cursor CURSOR FOR SELECT PublisherServer FROM @Publishers;
    OPEN publisher_cursor;
    FETCH NEXT FROM publisher_cursor INTO @Publisher;
    
    WHILE @@FETCH_STATUS = 0
    BEGIN
        IF @Verbose = 1 PRINT '  Querying: ' + @Publisher;
        
        BEGIN TRY
            IF EXISTS (SELECT 1 FROM sys.servers WHERE name = @Publisher)
            BEGIN
                SET @SQL = '
                INSERT INTO ctl.ReplicationMetrics (
                    PublisherServer, PublisherDatabase, PublicationName,
                    SubscriberServer, SubscriberDatabase,
                    PendingCmdCount, LatencySeconds, DeliveryRate,
                    AgentStatus, Status, CollectionTime
                )
                SELECT 
                    PublisherServer, PublisherDatabase, PublicationName,
                    SubscriberServer, SubscriberDatabase,
                    PendingCmds, LatencySeconds, DeliveryRate,
                    AgentStatus, Status, GETDATE()
                FROM OPENQUERY([' + @Publisher + '], 
                    ''EXEC distribution.dbo.usp_Get_ReplicationMetrics'');
                ';
                EXEC sp_executesql @SQL;
                
                IF @Verbose = 1 PRINT '    Collected ' + CAST(@@ROWCOUNT AS VARCHAR) + ' publications';
            END
            ELSE
            BEGIN
                INSERT INTO ctl.ReplicationMetrics (PublisherServer, Status, StatusMessage, CollectionTime)
                VALUES (@Publisher, 'Unknown', 'Linked server not configured', GETDATE());
            END
        END TRY
        BEGIN CATCH
            INSERT INTO ctl.ReplicationMetrics (PublisherServer, Status, StatusMessage, CollectionTime)
            VALUES (@Publisher, 'Error', 'Collection failed: ' + ERROR_MESSAGE(), GETDATE());
        END CATCH
        
        FETCH NEXT FROM publisher_cursor INTO @Publisher;
    END
    
    CLOSE publisher_cursor;
    DEALLOCATE publisher_cursor;
    
    -- Archive to history
    INSERT INTO fact.ReplicationMetricsHistory (
        PublisherServer, PublisherDatabase, PublicationName, SubscriberServer, SubscriberDatabase,
        PendingCmdCount, LatencySeconds, DeliveryRate, Status, CollectionTime
    )
    SELECT 
        PublisherServer, PublisherDatabase, PublicationName, SubscriberServer, SubscriberDatabase,
        PendingCmdCount, LatencySeconds, DeliveryRate, Status, CollectionTime
    FROM ctl.ReplicationMetrics;
    
    IF @Verbose = 1 PRINT 'Collection complete';
    
    SELECT 
        PublisherServer,
        COUNT(*) AS Publications,
        SUM(ISNULL(PendingCmdCount, 0)) AS TotalPendingCmds,
        MAX(ISNULL(LatencySeconds, 0)) AS MaxLatencySeconds,
        MAX(Status) AS WorstStatus
    FROM ctl.ReplicationMetrics
    GROUP BY PublisherServer
    ORDER BY PublisherServer;
END

GO

-- [ctl].[usp_Collect_ServerHealth]
-- Update the health collector to use CMS
CREATE   PROCEDURE ctl.usp_Collect_ServerHealth  
AS  
BEGIN  
    SET NOCOUNT ON;  
  
    INSERT INTO fact.ServerHealth (ServerName, TimeCaptured)  
    SELECT  
        s.Server AS ServerName,  
        SYSUTCDATETIME() AS TimeCaptured  
    FROM dbo.vw_CMS_Servers s;
END

GO

-- [ctl].[usp_Collect_ServerInventory]

CREATE PROCEDURE ctl.usp_Collect_ServerInventory
AS
BEGIN
    SET NOCOUNT ON;
    
    BEGIN TRY
        -- Update config.ServerInventory with local server info
        MERGE INTO config.ServerInventory AS target
        USING (
            SELECT 
                @@SERVERNAME AS ServerName,
                CAST(SERVERPROPERTY('MachineName') AS NVARCHAR(128)) AS InstanceName,
                CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(50)) AS SqlBuild,
                CAST(SERVERPROPERTY('ProductLevel') AS NVARCHAR(50)) AS SqlCumulativeUpdate,
                CAST(SERVERPROPERTY('Edition') AS NVARCHAR(128)) AS SQLEdition,
                (SELECT cpu_count FROM sys.dm_os_sys_info) AS CPUCount,
                (SELECT physical_memory_kb/1024 FROM sys.dm_os_sys_info) AS TotalMemoryMB,
                GETDATE() AS LastSeen
        ) AS source
        ON target.ServerName = source.ServerName
        WHEN MATCHED THEN
            UPDATE SET 
                SqlBuild = source.SqlBuild,
                SqlCumulativeUpdate = source.SqlCumulativeUpdate,
                SQLEdition = source.SQLEdition,
                CPUCount = source.CPUCount,
                TotalMemoryMB = source.TotalMemoryMB,
                LastSeen = source.LastSeen
        WHEN NOT MATCHED THEN
            INSERT (ServerName, InstanceName, SqlBuild, SqlCumulativeUpdate, SQLEdition, CPUCount, TotalMemoryMB, LastSeen, IsActive)
            VALUES (source.ServerName, source.InstanceName, source.SqlBuild, source.SqlCumulativeUpdate, source.SQLEdition, source.CPUCount, source.TotalMemoryMB, source.LastSeen, 1);
        
        PRINT 'Server inventory updated for: ' + @@SERVERNAME;
        
        -- Log to history if table exists and has correct columns
        IF OBJECT_ID('fact.ServerInventoryHistory', 'U') IS NOT NULL
        BEGIN
            DECLARE @SQL NVARCHAR(MAX);
            DECLARE @Cols NVARCHAR(500) = '';
            DECLARE @Vals NVARCHAR(1000) = '';
            
            IF EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = 'fact' AND TABLE_NAME = 'ServerInventoryHistory' AND COLUMN_NAME = 'ServerName')
            BEGIN SET @Cols = @Cols + 'ServerName, '; SET @Vals = @Vals + '@@SERVERNAME, '; END
            IF EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = 'fact' AND TABLE_NAME = 'ServerInventoryHistory' AND COLUMN_NAME = 'SQLVersion')
            BEGIN SET @Cols = @Cols + 'SQLVersion, '; SET @Vals = @Vals + 'CAST(SERVERPROPERTY(''ProductVersion'') AS NVARCHAR(50)), '; END
            IF EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = 'fact' AND TABLE_NAME = 'ServerInventoryHistory' AND COLUMN_NAME = 'SQLEdition')
            BEGIN SET @Cols = @Cols + 'SQLEdition, '; SET @Vals = @Vals + 'CAST(SERVERPROPERTY(''Edition'') AS NVARCHAR(128)), '; END
            IF EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = 'fact' AND TABLE_NAME = 'ServerInventoryHistory' AND COLUMN_NAME = 'CollectionTime')
            BEGIN SET @Cols = @Cols + 'CollectionTime, '; SET @Vals = @Vals + 'GETDATE(), '; END
            
            IF LEN(@Cols) > 2
            BEGIN
                SET @Cols = LEFT(@Cols, LEN(@Cols) - 1);
                SET @Vals = LEFT(@Vals, LEN(@Vals) - 1);
                SET @SQL = 'INSERT INTO fact.ServerInventoryHistory (' + @Cols + ') SELECT ' + @Vals;
                EXEC sp_executesql @SQL;
            END
        END
        
        PRINT 'Inventory collection complete';
    END TRY
    BEGIN CATCH
        PRINT 'Error: ' + ERROR_MESSAGE();
    END CATCH
END

GO

-- [ctl].[usp_Collect_ServiceCompliance]
CREATE OR ALTER PROCEDURE ctl.usp_Collect_ServiceCompliance
AS
BEGIN
    INSERT INTO ctl.ServiceCompliance 
    (
        ServerName, 
        ServiceName, 
        ServiceStatus, 
        StartType, 
        IsCompliant
    )
    SELECT 
        @@SERVERNAME,
        servicename,
        status_desc,
        startup_type_desc,
        CASE WHEN status_desc = 'Running' THEN 1 ELSE 0 END
    FROM sys.dm_server_services;
END;

GO

-- [ctl].[usp_Collect_VeeamRepositoryDiskSpace]

CREATE PROCEDURE ctl.usp_Collect_VeeamRepositoryDiskSpace
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @PSScriptPath NVARCHAR(500) = 'E:\DBAOps\Get-VeeamRepositoryDiskSpace.ps1';
    DECLARE @PSCommand NVARCHAR(4000);
    DECLARE @Result TABLE (OutputLine NVARCHAR(4000));
    
    -- Build PowerShell command to execute the script
    SET @PSCommand = 'powershell.exe -ExecutionPolicy Bypass -File "' + @PSScriptPath + '"';
    
    -- Execute PowerShell script and capture output
    INSERT INTO @Result (OutputLine)
    EXEC xp_cmdshell @PSCommand;
    
    -- Remove NULL rows
    DELETE FROM @Result WHERE OutputLine IS NULL OR OutputLine = '';
    
    -- Check for errors
    IF EXISTS (SELECT 1 FROM @Result WHERE OutputLine LIKE 'ERROR|%')
    BEGIN
        DECLARE @ErrorMsg NVARCHAR(1000);
        SELECT @ErrorMsg = OutputLine FROM @Result WHERE OutputLine LIKE 'ERROR|%';
        RAISERROR('Failed to collect disk space from AZVEEAM25: %s', 16, 1, @ErrorMsg);
        RETURN;
    END
    
    -- Check if we got any results
    IF NOT EXISTS (SELECT 1 FROM @Result WHERE OutputLine LIKE '%|%')
    BEGIN
        RAISERROR('No disk space data returned from PowerShell script. Check script path and permissions.', 16, 1);
        RETURN;
    END
    
    -- Parse results and update RepositoryAssignment
    ;WITH DiskData AS (
        SELECT 
            PARSENAME(REPLACE(OutputLine, '|', '.'), 4) AS DriveLetter,
            TRY_CAST(PARSENAME(REPLACE(OutputLine, '|', '.'), 3) AS DECIMAL(18,2)) AS TotalGB,
            TRY_CAST(PARSENAME(REPLACE(OutputLine, '|', '.'), 2) AS DECIMAL(18,2)) AS FreeGB,
            TRY_CAST(PARSENAME(REPLACE(OutputLine, '|', '.'), 1) AS DECIMAL(18,2)) AS FreePercent
        FROM @Result
        WHERE OutputLine NOT LIKE 'ERROR|%'
        AND OutputLine IS NOT NULL
        AND OutputLine LIKE '%|%'
    )
    UPDATE ra
    SET 
        RepoTotalTB = CAST(dd.TotalGB / 1024.0 AS DECIMAL(18,4)),
        RepoFreeTB = CAST(dd.FreeGB / 1024.0 AS DECIMAL(18,4)),
        RepoFreePercent = dd.FreePercent,
        LastUpdated = GETDATE()
    FROM [config].[RepositoryAssignment] ra
    INNER JOIN DiskData dd ON ra.RepoVolume = dd.DriveLetter;
    
    -- Log results
    DECLARE @UpdatedCount INT = @@ROWCOUNT;
    PRINT 'Updated ' + CAST(@UpdatedCount AS VARCHAR(10)) + ' repository records from AZVEEAM25';
    
    -- Show current state
    SELECT 
        RepositoryID,
        RepositoryName,
        RepoVolume,
        RepoTotalTB,
        RepoFreeTB,
        RepoFreePercent,
        LastUpdated
    FROM [config].[RepositoryAssignment]
    ORDER BY RepositoryID;
    
END

GO

-- [ctl].[usp_DataRetention_Cleanup]

CREATE PROCEDURE ctl.usp_DataRetention_Cleanup
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @SQL NVARCHAR(MAX);
    DECLARE @DateColumn NVARCHAR(128);
    DECLARE @RowsDeleted INT;
    
    PRINT 'Starting Data Retention Cleanup - ' + CONVERT(VARCHAR, GETDATE(), 120);
    
    -- 1. fact.BackupCompliance (keep 30 days)
    SELECT TOP 1 @DateColumn = COLUMN_NAME
    FROM INFORMATION_SCHEMA.COLUMNS
    WHERE TABLE_SCHEMA = 'fact' AND TABLE_NAME = 'BackupCompliance'
    AND DATA_TYPE IN ('datetime', 'datetime2', 'date')
    ORDER BY ORDINAL_POSITION;
    
    IF @DateColumn IS NOT NULL
    BEGIN
        SET @SQL = 'DELETE FROM fact.BackupCompliance WHERE ' + QUOTENAME(@DateColumn) + ' < DATEADD(DAY, -30, GETDATE())';
        EXEC sp_executesql @SQL;
        PRINT 'Cleaned fact.BackupCompliance: ' + CAST(@@ROWCOUNT AS VARCHAR) + ' rows';
    END
    
    -- 2. ctl.BackupCompliance (keep 7 days)
    SELECT TOP 1 @DateColumn = COLUMN_NAME
    FROM INFORMATION_SCHEMA.COLUMNS
    WHERE TABLE_SCHEMA = 'ctl' AND TABLE_NAME = 'BackupCompliance'
    AND DATA_TYPE IN ('datetime', 'datetime2', 'date')
    ORDER BY ORDINAL_POSITION;
    
    IF @DateColumn IS NOT NULL
    BEGIN
        SET @SQL = 'DELETE FROM ctl.BackupCompliance WHERE ' + QUOTENAME(@DateColumn) + ' < DATEADD(DAY, -7, GETDATE())';
        EXEC sp_executesql @SQL;
        PRINT 'Cleaned ctl.BackupCompliance: ' + CAST(@@ROWCOUNT AS VARCHAR) + ' rows';
    END
    
    -- 3. fact.MemoryCompliance (keep 30 days)
    IF OBJECT_ID('fact.MemoryCompliance', 'U') IS NOT NULL
    BEGIN
        SELECT TOP 1 @DateColumn = COLUMN_NAME
        FROM INFORMATION_SCHEMA.COLUMNS
        WHERE TABLE_SCHEMA = 'fact' AND TABLE_NAME = 'MemoryCompliance'
        AND DATA_TYPE IN ('datetime', 'datetime2', 'date')
        ORDER BY ORDINAL_POSITION;
        
        IF @DateColumn IS NOT NULL
        BEGIN
            SET @SQL = 'DELETE FROM fact.MemoryCompliance WHERE ' + QUOTENAME(@DateColumn) + ' < DATEADD(DAY, -30, GETDATE())';
            EXEC sp_executesql @SQL;
            PRINT 'Cleaned fact.MemoryCompliance: ' + CAST(@@ROWCOUNT AS VARCHAR) + ' rows';
        END
    END
    
    -- 4. audit.AuditLog (keep 90 days)
    IF OBJECT_ID('audit.AuditLog', 'U') IS NOT NULL
    BEGIN
        SELECT TOP 1 @DateColumn = COLUMN_NAME
        FROM INFORMATION_SCHEMA.COLUMNS
        WHERE TABLE_SCHEMA = 'audit' AND TABLE_NAME = 'AuditLog'
        AND DATA_TYPE IN ('datetime', 'datetime2', 'date')
        ORDER BY ORDINAL_POSITION;
        
        IF @DateColumn IS NOT NULL
        BEGIN
            SET @SQL = 'DELETE FROM audit.AuditLog WHERE ' + QUOTENAME(@DateColumn) + ' < DATEADD(DAY, -90, GETDATE())';
            EXEC sp_executesql @SQL;
            PRINT 'Cleaned audit.AuditLog: ' + CAST(@@ROWCOUNT AS VARCHAR) + ' rows';
        END
    END
    
    PRINT 'Data Retention Cleanup Complete';
END

GO

-- [ctl].[usp_DetectInventoryChanges]

CREATE   PROCEDURE ctl.usp_DetectInventoryChanges
AS
BEGIN
    SET NOCOUNT ON;
    
    -- Detect changes using CMS as source
    SELECT 
        cms.ServerName,
        COALESCE(cms.Environment, cms.CMSEnvironment, 'Unknown') AS Environment,
        cms.SQLVersion,
        cms.LastInventoryUpdate,
        cms.ModifiedDate
    FROM config.ServerInventory cms
    WHERE cms.ModifiedDate >= DATEADD(DAY, -1, GETDATE())
    ORDER BY cms.ModifiedDate DESC;
END

GO

-- [ctl].[usp_EnforceDataRetention]

CREATE   PROCEDURE ctl.usp_EnforceDataRetention
    @Debug BIT = 0,
    @DryRun BIT = 0  -- If 1, shows what would be deleted without actually deleting
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @SchemaName VARCHAR(128);
    DECLARE @TableName VARCHAR(128);
    DECLARE @DateColumn VARCHAR(128);
    DECLARE @RetentionDays INT;
    DECLARE @RetentionID INT;
    DECLARE @SQL NVARCHAR(MAX);
    DECLARE @RowsDeleted INT;
    DECLARE @TotalRowsDeleted INT = 0;
    DECLARE @TablesProcessed INT = 0;
    DECLARE @CutoffDate DATETIME2;
    DECLARE @StartTime DATETIME2 = SYSDATETIME();
    DECLARE @ErrorMessage NVARCHAR(4000);
    
    -- Results table
    CREATE TABLE #RetentionResults (
        SchemaName VARCHAR(128),
        TableName VARCHAR(128),
        RetentionDays INT,
        CutoffDate DATETIME2,
        RowsDeleted INT,
        Status VARCHAR(50),
        ErrorMessage NVARCHAR(500)
    );
    
    PRINT '=============================================================';
    PRINT 'DBAOps Data Retention Enforcement';
    PRINT 'Started: ' + CONVERT(VARCHAR, @StartTime, 120);
    PRINT 'Mode: ' + CASE WHEN @DryRun = 1 THEN 'DRY RUN (No deletions)' ELSE 'LIVE' END;
    PRINT '=============================================================';
    
    -- Cursor through all active retention policies
    DECLARE retention_cursor CURSOR LOCAL FAST_FORWARD FOR
        SELECT RetentionID, SchemaName, TableName, DateColumn, RetentionDays
        FROM config.DataRetention
        WHERE IsActive = 1
        ORDER BY RetentionDays, SchemaName, TableName;
    
    OPEN retention_cursor;
    FETCH NEXT FROM retention_cursor INTO @RetentionID, @SchemaName, @TableName, @DateColumn, @RetentionDays;
    
    WHILE @@FETCH_STATUS = 0
    BEGIN
        SET @RowsDeleted = 0;
        SET @CutoffDate = DATEADD(DAY, -@RetentionDays, GETDATE());
        
        BEGIN TRY
            -- Check if table exists
            IF NOT EXISTS (
                SELECT 1 FROM sys.tables t 
                JOIN sys.schemas s ON t.schema_id = s.schema_id
                WHERE s.name = @SchemaName AND t.name = @TableName
            )
            BEGIN
                INSERT INTO #RetentionResults VALUES (@SchemaName, @TableName, @RetentionDays, @CutoffDate, 0, 'TABLE NOT FOUND', 'Table does not exist');
                IF @Debug = 1 PRINT '  SKIP: ' + @SchemaName + '.' + @TableName + ' - Table not found';
            END
            ELSE
            BEGIN
                -- Check if column exists
                IF NOT EXISTS (
                    SELECT 1 FROM sys.columns c
                    JOIN sys.tables t ON c.object_id = t.object_id
                    JOIN sys.schemas s ON t.schema_id = s.schema_id
                    WHERE s.name = @SchemaName AND t.name = @TableName AND c.name = @DateColumn
                )
                BEGIN
                    INSERT INTO #RetentionResults VALUES (@SchemaName, @TableName, @RetentionDays, @CutoffDate, 0, 'COLUMN NOT FOUND', 'Column ' + @DateColumn + ' does not exist');
                    IF @Debug = 1 PRINT '  SKIP: ' + @SchemaName + '.' + @TableName + ' - Column ' + @DateColumn + ' not found';
                END
                ELSE
                BEGIN
                    IF @DryRun = 1
                    BEGIN
                        -- Count what would be deleted
                        SET @SQL = N'SELECT @Cnt = COUNT(*) FROM ' + QUOTENAME(@SchemaName) + '.' + QUOTENAME(@TableName) + 
                                   N' WHERE ' + QUOTENAME(@DateColumn) + ' < @Cutoff';
                        EXEC sp_executesql @SQL, N'@Cutoff DATETIME2, @Cnt INT OUTPUT', @CutoffDate, @RowsDeleted OUTPUT;
                        
                        INSERT INTO #RetentionResults VALUES (@SchemaName, @TableName, @RetentionDays, @CutoffDate, @RowsDeleted, 'DRY RUN', NULL);
                        IF @Debug = 1 PRINT '  DRY RUN: ' + @SchemaName + '.' + @TableName + ' - Would delete ' + CAST(@RowsDeleted AS VARCHAR) + ' rows';
                    END
                    ELSE
                    BEGIN
                        -- Actually delete in batches to avoid log bloat
                        DECLARE @BatchSize INT = 10000;
                        DECLARE @BatchDeleted INT = 1;
                        SET @RowsDeleted = 0;
                        
                        WHILE @BatchDeleted > 0
                        BEGIN
                            SET @SQL = N'DELETE TOP (' + CAST(@BatchSize AS VARCHAR) + ') FROM ' + 
                                       QUOTENAME(@SchemaName) + '.' + QUOTENAME(@TableName) + 
                                       N' WHERE ' + QUOTENAME(@DateColumn) + ' < @Cutoff';
                            
                            EXEC sp_executesql @SQL, N'@Cutoff DATETIME2', @CutoffDate;
                            SET @BatchDeleted = @@ROWCOUNT;
                            SET @RowsDeleted = @RowsDeleted + @BatchDeleted;
                            
                            -- Brief pause to reduce pressure
                            IF @BatchDeleted = @BatchSize
                                WAITFOR DELAY '00:00:00.100';
                        END
                        
                        -- Update tracking
                        UPDATE config.DataRetention
                        SET LastCleanupDate = SYSDATETIME(),
                            LastRowsDeleted = @RowsDeleted
                        WHERE RetentionID = @RetentionID;
                        
                        INSERT INTO #RetentionResults VALUES (@SchemaName, @TableName, @RetentionDays, @CutoffDate, @RowsDeleted, 'SUCCESS', NULL);
                        IF @Debug = 1 PRINT '  SUCCESS: ' + @SchemaName + '.' + @TableName + ' - Deleted ' + CAST(@RowsDeleted AS VARCHAR) + ' rows';
                        
                        SET @TotalRowsDeleted = @TotalRowsDeleted + @RowsDeleted;
                    END
                END
            END
            
            SET @TablesProcessed = @TablesProcessed + 1;
            
        END TRY
        BEGIN CATCH
            SET @ErrorMessage = ERROR_MESSAGE();
            INSERT INTO #RetentionResults VALUES (@SchemaName, @TableName, @RetentionDays, @CutoffDate, 0, 'ERROR', @ErrorMessage);
            IF @Debug = 1 PRINT '  ERROR: ' + @SchemaName + '.' + @TableName + ' - ' + @ErrorMessage;
        END CATCH
        
        FETCH NEXT FROM retention_cursor INTO @RetentionID, @SchemaName, @TableName, @DateColumn, @RetentionDays;
    END
    
    CLOSE retention_cursor;
    DEALLOCATE retention_cursor;
    
    -- Display results
    PRINT '';
    PRINT '=============================================================';
    PRINT 'RETENTION ENFORCEMENT COMPLETE';
    PRINT 'Duration: ' + CAST(DATEDIFF(SECOND, @StartTime, SYSDATETIME()) AS VARCHAR) + ' seconds';
    PRINT 'Tables Processed: ' + CAST(@TablesProcessed AS VARCHAR);
    PRINT 'Total Rows ' + CASE WHEN @DryRun = 1 THEN 'Would Be ' ELSE '' END + 'Deleted: ' + CAST(@TotalRowsDeleted AS VARCHAR);
    PRINT '=============================================================';
    
    -- Return results
    SELECT 
        SchemaName + '.' + TableName AS [Table],
        RetentionDays,
        FORMAT(CutoffDate, 'yyyy-MM-dd') AS CutoffDate,
        RowsDeleted,
        Status,
        ErrorMessage
    FROM #RetentionResults
    ORDER BY 
        CASE Status WHEN 'ERROR' THEN 1 WHEN 'TABLE NOT FOUND' THEN 2 WHEN 'COLUMN NOT FOUND' THEN 3 ELSE 4 END,
        RowsDeleted DESC;
    
    DROP TABLE #RetentionResults;
    
    RETURN @TotalRowsDeleted;
END;

GO

-- [ctl].[usp_Fact_AutoHeal_All]

CREATE PROCEDURE ctl.usp_Fact_AutoHeal_All
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @RowsAffected INT = 0;
    
    -- Refresh BackupHealth from BackupHistory using correct column names
    MERGE fact.BackupHealth AS target
    USING (
        SELECT 
            ServerName,
            DatabaseName,
            MAX(CASE WHEN BackupType IN ('F', 'Full', 'FULL') THEN BackupFinishTime END) AS LastFull,
            MAX(CASE WHEN BackupType IN ('D', 'Diff', 'DIFF') THEN BackupFinishTime END) AS LastDiff,
            MAX(CASE WHEN BackupType IN ('L', 'Log', 'LOG') THEN BackupFinishTime END) AS LastLog
        FROM fact.BackupHistory
        WHERE BackupFinishTime >= DATEADD(DAY, -7, GETDATE())
        GROUP BY ServerName, DatabaseName
    ) AS source
    ON target.ServerName = source.ServerName AND target.DatabaseName = source.DatabaseName
    WHEN MATCHED THEN
        UPDATE SET 
            LastFullBackup = source.LastFull,
            LastDiffBackup = source.LastDiff,
            LastLogBackup = source.LastLog,
            LastBackupTime = COALESCE(source.LastFull, source.LastDiff, source.LastLog),
            IsCompliant = CASE WHEN COALESCE(source.LastFull, source.LastDiff) >= DATEADD(HOUR, -24, GETDATE()) THEN 1 ELSE 0 END,
            LoadDate = SYSDATETIME()
    WHEN NOT MATCHED BY TARGET THEN
        INSERT (ServerName, DatabaseName, LastBackupTime, LastFullBackup, LastDiffBackup, LastLogBackup, IsCompliant)
        VALUES (source.ServerName, source.DatabaseName, 
                COALESCE(source.LastFull, source.LastDiff, source.LastLog),
                source.LastFull, source.LastDiff, source.LastLog,
                CASE WHEN COALESCE(source.LastFull, source.LastDiff) >= DATEADD(HOUR, -24, GETDATE()) THEN 1 ELSE 0 END);
    
    SET @RowsAffected = @@ROWCOUNT;
    
    PRINT 'FACT AutoHeal completed. BackupHealth rows affected: ' + CAST(@RowsAffected AS VARCHAR);
END

GO

-- [ctl].[usp_Fact_Report_Daily]

CREATE PROCEDURE ctl.usp_Fact_Report_Daily
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @HTML NVARCHAR(MAX) = N'
        <h2 style="color:#003366;">DBAOps â€“ FACT Daily Summary</h2>
        <p>Environment: <b>YOURORG SQL Server</b></p>
        <p>Generated: ' + CONVERT(VARCHAR(30), SYSDATETIME(), 120) + '</p><hr>';

    /*============================
      SERVER HEALTH SUMMARY
    ============================*/
    SET @HTML += N'
        <h3>ServerHealth</h3>
        <table border="1" cellpadding="5" cellspacing="0" style="border-collapse:collapse;">
        <tr>
            <th>ServerName</th>
            <th>CPU %</th>
            <th>Memory %</th>
            <th>TimeCaptured</th>
        </tr>';

    SELECT @HTML += '
        <tr>
            <td>'+ServerName+'</td>
            <td>'+CAST(CPUPercent AS VARCHAR(10))+'</td>
            <td>'+CAST(MemoryPercent AS VARCHAR(10))+'</td>
            <td>'+CONVERT(VARCHAR(30),TimeCaptured,120)+'</td>
        </tr>'
    FROM fact.ServerHealth;

    SET @HTML += '</table><br>';

    /*============================
      PERFORMANCE SUMMARY
    ============================*/
    SET @HTML += N'
        <h3>Performance</h3>
        <table border="1" cellpadding="5" cellspacing="0" style="border-collapse:collapse;">
        <tr>
            <th>ServerName</th>
            <th>BatchReq/Sec</th>
            <th>Waits/Sec</th>
            <th>TimeCaptured</th>
        </tr>';

    SELECT @HTML += '
        <tr>
            <td>'+ServerName+'</td>
            <td>'+CAST(BatchRequestsPS AS VARCHAR(10))+'</td>
            <td>'+CAST(WaitsPerSec AS VARCHAR(10))+'</td>
            <td>'+CONVERT(VARCHAR(30),TimeCaptured,120)+'</td>
        </tr>'
    FROM fact.Performance;

    SET @HTML += '</table><br>';

    /*============================
      DISK SPACE SUMMARY
    ============================*/
    SET @HTML += N'
        <h3>Disk Space</h3>
        <table border="1" cellpadding="5" cellspacing="0" style="border-collapse:collapse;">
        <tr>
            <th>ServerName</th>
            <th>Drive</th>
            <th>TotalGB</th>
            <th>FreeGB</th>
            <th>TimeCaptured</th>
        </tr>';

    SELECT @HTML += '
        <tr>
            <td>'+ServerName+'</td>
            <td>'+DriveLetter+'</td>
            <td>'+CAST(TotalGB AS VARCHAR(10))+'</td>
            <td>'+CAST(FreeGB AS VARCHAR(10))+'</td>
            <td>'+CONVERT(VARCHAR(30),TimeCaptured,120)+'</td>
        </tr>'
    FROM fact.DiskSpace;

    SET @HTML += '</table><br>';

    /*============================
      MEMORY COMPLIANCE SUMMARY
    ============================*/
    SET @HTML += N'
        <h3>Memory Compliance</h3>
        <table border="1" cellpadding="5" cellspacing="0" style="border-collapse:collapse;">
        <tr>
            <th>ServerName</th>
            <th>TimeCaptured</th>
        </tr>';

    SELECT @HTML += '
        <tr>
            <td>'+ServerName+'</td>
            <td>'+CONVERT(VARCHAR(30),TimeCaptured,120)+'</td>
        </tr>'
    FROM fact.MemoryCompliance;

    SET @HTML += '</table><br><hr><p>End of Report</p>';

    EXEC ctl.usp_Fact_Report_SendEmail
        @Body = @HTML,
        @Subject = 'DBAOps â€“ FACT Daily Report';

END;

GO

-- [ctl].[usp_Fact_Report_SendEmail]

CREATE PROCEDURE ctl.usp_Fact_Report_SendEmail
(
    @Body       NVARCHAR(MAX),
    @Subject    NVARCHAR(200),
    @Recipients NVARCHAR(400) = 'SQLServerAdminNotification@YOURORG.gov'
)
AS
BEGIN
    SET NOCOUNT ON;

    BEGIN TRY
        EXEC msdb.dbo.sp_send_dbmail
            @profile_name = 'DBAOpsProfile',
            @recipients   = @Recipients,
            @subject      = @Subject,
            @body         = @Body,
            @body_format  = 'HTML';

        INSERT INTO meta.FactReportLog(Status, SentTo)
        VALUES ('SUCCESS', @Recipients);
    END TRY
    BEGIN CATCH
        INSERT INTO meta.FactReportLog(Status, SentTo, ErrorMessage)
        VALUES ('FAILED', @Recipients, ERROR_MESSAGE());
    END CATCH
END;

GO

-- [ctl].[usp_Fact_Validate_DiskSpace]

CREATE PROCEDURE ctl.usp_Fact_Validate_DiskSpace
AS
BEGIN
    SET NOCOUNT ON;

    INSERT INTO meta.FactAudit (FactName, ValidationStatus, IssueCount)
    SELECT 'DiskSpace',
           CASE WHEN COUNT(*) = 0 THEN 'FAILED' ELSE 'OK' END,
           (SELECT COUNT(*) FROM fact.DiskSpace)
    FROM fact.DiskSpace;
END;

GO

-- [ctl].[usp_Fact_Validate_MemoryCompliance]

CREATE PROCEDURE ctl.usp_Fact_Validate_MemoryCompliance
AS
BEGIN
    SET NOCOUNT ON;

    INSERT INTO meta.FactAudit (FactName, ValidationStatus, IssueCount)
    SELECT 'MemoryCompliance',
           CASE WHEN COUNT(*) = 0 THEN 'FAILED' ELSE 'OK' END,
           (SELECT COUNT(*) FROM fact.MemoryCompliance)
    FROM fact.MemoryCompliance;
END;

GO

-- [ctl].[usp_Fact_Validate_Performance]

CREATE PROCEDURE ctl.usp_Fact_Validate_Performance
AS
BEGIN
    SET NOCOUNT ON;

    INSERT INTO meta.FactAudit (FactName, ValidationStatus, IssueCount)
    SELECT 'Performance',
           CASE WHEN COUNT(*) = 0 THEN 'FAILED' ELSE 'OK' END,
           (SELECT COUNT(*) FROM fact.Performance)
    FROM fact.Performance;
END;

GO

-- [ctl].[usp_Fact_Validate_ServerHealth]

CREATE PROCEDURE ctl.usp_Fact_Validate_ServerHealth
AS
BEGIN
    SET NOCOUNT ON;

    INSERT INTO meta.FactAudit (FactName, ValidationStatus, IssueCount)
    SELECT 'ServerHealth',
           CASE WHEN COUNT(*) = 0 THEN 'FAILED' ELSE 'OK' END,
           (SELECT COUNT(*) FROM fact.ServerHealth)
    FROM fact.ServerHealth;
END;

GO

-- [ctl].[usp_Insert_BackupCompliance]
CREATE OR ALTER PROCEDURE ctl.usp_Insert_BackupCompliance  
(
    @ServerName SYSNAME,
    @DatabaseName SYSNAME,
    @BackupType VARCHAR(20),
    @LastBackupTime DATETIME,
    @IsCompliant BIT
)
AS
BEGIN
    SET NOCOUNT ON;

    BEGIN TRY
        INSERT INTO ctl.BackupCompliance
        (
            ServerName,
            DatabaseName,
            BackupType,
            LastBackupTime,
            IsCompliant
        )
        VALUES
        (
            @ServerName,
            @DatabaseName,
            @BackupType,
            @LastBackupTime,
            @IsCompliant
        );
    END TRY
    BEGIN CATCH
        DECLARE @Err NVARCHAR(MAX) = ERROR_MESSAGE();
        RAISERROR('usp_Insert_BackupCompliance failed: %s', 16, 1, @Err);
    END CATCH;
END;

GO

-- [ctl].[usp_InsertBackupComplianceRecord]
CREATE OR ALTER PROCEDURE ctl.usp_InsertBackupComplianceRecord  
    @BackupSource VARCHAR(20),
    @ServerName VARCHAR(128),
    @VmName VARCHAR(128) = NULL,
    @DatabaseName VARCHAR(256) = NULL,
    @BackupType VARCHAR(50),
    @LastBackupTime DATETIME = NULL,
    @NextExpectedBackupTime DATETIME = NULL,
    @JobName VARCHAR(256) = NULL,
    @Status VARCHAR(50) = NULL,
    @BackupMode VARCHAR(50) = NULL,
    @BackupSizeGB DECIMAL(18,2) = NULL,
    @Repository VARCHAR(128) = NULL,
    @RepositoryVolume VARCHAR(5) = NULL,
    @RepoTotalTB DECIMAL(18,2) = NULL,
    @RepoFreeTB DECIMAL(18,2) = NULL,
    @IsCompliant BIT = 0
AS
BEGIN
    SET NOCOUNT ON;

    BEGIN TRY
        INSERT INTO ctl.BackupCompliance 
        (
            BackupSource,
            ServerName,
            VmName,
            DatabaseName,
            BackupType,
            LastBackupTime,
            NextExpectedBackupTime,
            JobName,
            Status,
            BackupMode,
            BackupSizeGB,
            Repository,
            RepositoryVolume,
            RepoTotalTB,
            RepoFreeTB,
            IsCompliant
        )
        VALUES
        (
            @BackupSource,
            @ServerName,
            @VmName,
            @DatabaseName,
            @BackupType,
            @LastBackupTime,
            @NextExpectedBackupTime,
            @JobName,
            @Status,
            @BackupMode,
            @BackupSizeGB,
            @Repository,
            @RepositoryVolume,
            @RepoTotalTB,
            @RepoFreeTB,
            @IsCompliant
        );
    END TRY
    BEGIN CATCH
        DECLARE @Err NVARCHAR(MAX) = ERROR_MESSAGE();
        RAISERROR('usp_InsertBackupComplianceRecord failed: %s', 16, 1, @Err);
    END CATCH;
END;

GO

-- [ctl].[usp_JobComplianceReport]

-- ============================================================================
-- STEP 3: Create HTML Report procedure for alerting
-- ============================================================================
CREATE   PROCEDURE ctl.usp_JobComplianceReport
    @SendEmail BIT = 1,
    @Recipients VARCHAR(500) = 'DBAOps@YOURORG.gov; SQLServerAdminNotification@YOURORG.gov'
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @HTML NVARCHAR(MAX);
    DECLARE @Subject NVARCHAR(255);
    DECLARE @NonCompliantCount INT;
    DECLARE @FailedCount INT;
    DECLARE @NeverRunCount INT;
    
    -- Get counts
    SELECT 
        @NonCompliantCount = SUM(CASE WHEN IsCompliant = 0 THEN 1 ELSE 0 END),
        @FailedCount = SUM(CASE WHEN LastRunStatus = 'Failed' THEN 1 ELSE 0 END)
    FROM ctl.JobCompliance
    WHERE ServerName = @@SERVERNAME
      AND CAST(LastRunDateTime AS DATE) >= CAST(DATEADD(DAY, -1, GETDATE()) AS DATE);
    
    SELECT @NeverRunCount = COUNT(*)
    FROM msdb.dbo.sysjobs j
    WHERE j.enabled = 1
      AND NOT EXISTS (
          SELECT 1 FROM msdb.dbo.sysjobhistory h 
          WHERE h.job_id = j.job_id AND h.step_id = 0
      );
    
    SET @Subject = CONCAT('SQL Agent Job Compliance Report - ', @@SERVERNAME, 
        ' [', @FailedCount, ' Failed, ', @NeverRunCount, ' Never Run]');
    
    -- Build HTML Report
    SET @HTML = N'
<!DOCTYPE html>
<html>
<head>
<style>
    body { font-family: Segoe UI, Arial, sans-serif; font-size: 12px; }
    h2 { color: #2E86AB; margin-bottom: 5px; }
    h3 { color: #A23B72; margin-top: 20px; margin-bottom: 5px; }
    table { border-collapse: collapse; width: 100%; margin-bottom: 20px; }
    th { background-color: #2E86AB; color: white; padding: 8px; text-align: left; }
    td { padding: 6px 8px; border-bottom: 1px solid #ddd; }
    tr:nth-child(even) { background-color: #f9f9f9; }
    .failed { background-color: #FFCCCC; }
    .warning { background-color: #FFFFCC; }
    .success { background-color: #CCFFCC; }
    .summary { background-color: #f0f0f0; padding: 10px; margin-bottom: 15px; border-radius: 5px; }
    .timestamp { color: #666; font-size: 11px; }
</style>
</head>
<body>
<h2>ðŸ” SQL Agent Job Compliance Report</h2>
<div class="summary">
    <strong>Server:</strong> ' + @@SERVERNAME + N'<br>
    <strong>Report Time:</strong> ' + CONVERT(NVARCHAR(30), GETDATE(), 120) + N'<br>
    <strong>Failed Jobs (24h):</strong> ' + CAST(ISNULL(@FailedCount, 0) AS NVARCHAR(10)) + N'<br>
    <strong>Jobs Never Run:</strong> ' + CAST(ISNULL(@NeverRunCount, 0) AS NVARCHAR(10)) + N'
</div>';

    -- Failed Jobs Section
    IF @FailedCount > 0
    BEGIN
        SET @HTML = @HTML + N'
<h3>âŒ Failed Jobs (Last 24 Hours)</h3>
<table>
<tr><th>Job Name</th><th>Last Run</th><th>Status</th><th>Hours Ago</th></tr>';
        
        SELECT @HTML = @HTML + N'
<tr class="failed">
    <td>' + JobName + N'</td>
    <td>' + ISNULL(CONVERT(NVARCHAR(30), LastRunDateTime, 120), 'N/A') + N'</td>
    <td>' + ISNULL(LastRunStatus, 'Unknown') + N'</td>
    <td>' + CAST(ISNULL(DATEDIFF(HOUR, LastRunDateTime, GETDATE()), 0) AS NVARCHAR(10)) + N'</td>
</tr>'
        FROM ctl.JobCompliance
        WHERE ServerName = @@SERVERNAME
          AND LastRunStatus = 'Failed'
          AND LastRunDateTime >= DATEADD(DAY, -1, GETDATE())
        ORDER BY LastRunDateTime DESC;
        
        SET @HTML = @HTML + N'</table>';
    END

    -- Never Run Jobs Section
    IF @NeverRunCount > 0
    BEGIN
        SET @HTML = @HTML + N'
<h3>âš ï¸ Jobs Never Run (Enabled but no history)</h3>
<table>
<tr><th>Job Name</th><th>Created Date</th><th>Owner</th></tr>';
        
        SELECT @HTML = @HTML + N'
<tr class="warning">
    <td>' + j.name + N'</td>
    <td>' + CONVERT(NVARCHAR(30), j.date_created, 120) + N'</td>
    <td>' + SUSER_SNAME(j.owner_sid) + N'</td>
</tr>'
        FROM msdb.dbo.sysjobs j
        WHERE j.enabled = 1
          AND j.name NOT LIKE 'syspolicy%'
          AND NOT EXISTS (
              SELECT 1 FROM msdb.dbo.sysjobhistory h 
              WHERE h.job_id = j.job_id AND h.step_id = 0
          )
        ORDER BY j.name;
        
        SET @HTML = @HTML + N'</table>';
    END

    -- Successful Jobs Summary (top 10 most recent)
    SET @HTML = @HTML + N'
<h3>âœ… Recent Successful Jobs (Last 10)</h3>
<table>
<tr><th>Job Name</th><th>Last Run</th><th>Status</th></tr>';
    
    SELECT TOP 10 @HTML = @HTML + N'
<tr class="success">
    <td>' + JobName + N'</td>
    <td>' + ISNULL(CONVERT(NVARCHAR(30), LastRunDateTime, 120), 'N/A') + N'</td>
    <td>' + ISNULL(LastRunStatus, 'Unknown') + N'</td>
</tr>'
    FROM ctl.JobCompliance
    WHERE ServerName = @@SERVERNAME
      AND LastRunStatus = 'Succeeded'
      AND LastRunDateTime >= DATEADD(DAY, -1, GETDATE())
    ORDER BY LastRunDateTime DESC;
    
    SET @HTML = @HTML + N'</table>';

    -- Footer
    SET @HTML = @HTML + N'
<p class="timestamp">Generated by DBAOps Enterprise™ Framework on ' + @@SERVERNAME + N'</p>
</body>
</html>';

    -- Send email if requested
    IF @SendEmail = 1 AND (@FailedCount > 0 OR @NeverRunCount > 0)
    BEGIN
        EXEC msdb.dbo.sp_send_dbmail
            @profile_name = 'DBAOpsProfile',
            @recipients = @Recipients,
            @subject = @Subject,
            @body = @HTML,
            @body_format = 'HTML';
            
        PRINT 'Compliance report email sent.';
    END
    ELSE IF @SendEmail = 1
    BEGIN
        PRINT 'All jobs compliant - no email sent.';
    END
    
    -- Return the HTML for display
    SELECT @HTML AS HTMLReport;
END;

GO

-- [ctl].[usp_Load_BackupCompliance]

CREATE PROCEDURE ctl.usp_Load_BackupCompliance      
AS      
BEGIN      
    SET NOCOUNT ON;      
          
    PRINT 'Loading backup compliance - Veeam Backup Policy';      
          
    IF OBJECT_ID('ctl.BackupCompliance', 'U') IS NOT NULL      
    BEGIN      
        -- YOURORG Veeam Backup Policy:
        -- PROD: Full backup twice a week (every 3.5 days = 84 hours) + hourly tlog
        -- NONPROD: Full backup once a week (7 days = 168 hours), no tlog
        
        UPDATE bc    
        SET Status = CASE     
                -- Handle future timestamps (timezone issues)
                WHEN DATEDIFF(HOUR, bc.LastBackupTime, GETDATE()) < 0 THEN 'Success'
                
                -- PROD Servers: Full backup twice weekly (every 3.5 days)
                -- â‰¤84 hours (3.5 days) = Success
                -- â‰¤108 hours (4.5 days) = Warning (grace period)
                -- >108 hours = Stale (missed scheduled backup)
                WHEN ISNULL(si.Environment, 'PROD') = 'PROD' AND DATEDIFF(HOUR, bc.LastBackupTime, GETDATE()) <= 84 THEN 'Success'    
                WHEN ISNULL(si.Environment, 'PROD') = 'PROD' AND DATEDIFF(HOUR, bc.LastBackupTime, GETDATE()) <= 108 THEN 'Warning'    
                WHEN ISNULL(si.Environment, 'PROD') = 'PROD' AND DATEDIFF(HOUR, bc.LastBackupTime, GETDATE()) > 108 THEN 'Stale'    
                    
                -- NON-PROD Servers: Full backup once weekly (7 days)
                -- â‰¤168 hours (7 days) = Success
                -- â‰¤240 hours (10 days) = Warning (grace period)
                -- >240 hours = Stale
                WHEN si.Environment = 'NONPROD' AND DATEDIFF(HOUR, bc.LastBackupTime, GETDATE()) <= 168 THEN 'Success'    
                WHEN si.Environment = 'NONPROD' AND DATEDIFF(HOUR, bc.LastBackupTime, GETDATE()) <= 240 THEN 'Warning'    
                WHEN si.Environment = 'NONPROD' AND DATEDIFF(HOUR, bc.LastBackupTime, GETDATE()) > 240 THEN 'Stale'    
                    
                ELSE 'Success'
            END  
        FROM ctl.BackupCompliance bc    
        LEFT JOIN config.ServerInventory si ON bc.ServerName = si.ServerName;    
          
        -- Update IsCompliant based on Status
        UPDATE ctl.BackupCompliance  
        SET IsCompliant = CASE   
                WHEN Status IN ('Success', 'Warning') THEN 1   
                ELSE 0   
            END;  
              
        PRINT 'Updated ' + CAST(@@ROWCOUNT AS VARCHAR) + ' records';      
    END      
          
    PRINT 'Complete';      
END

GO

-- [ctl].[usp_Load_BackupCompliance_Safe]

CREATE PROCEDURE ctl.usp_Load_BackupCompliance_Safe
AS
BEGIN
    SET NOCOUNT ON;
    
    BEGIN TRY
        -- Insert with NULL handling
        INSERT INTO fact.BackupCompliance (
            ServerName,
            DatabaseName,
            BackupType,
            LastBackupTime,
            AgeMinutes,
            SLA_Minutes,
            Status,
            LoadDate,
            BackupSource,
            VmName,
            NextExpectedBackupTime,
            JobName,
            BackupMode,
            BackupSizeGB,
            Repository,
            RepositoryVolume,
            RepoTotalTB,
            RepoFreeTB,
            ExpectedFrequency,
            IsCompliant,
            SnapshotDate,
            SnapshotHour,
            DateKey,
            ComplianceScore,
            RepoFreePercent
        )
        SELECT 
            ISNULL(c.ServerName, 'Unknown'),
            ISNULL(c.DatabaseName, 'N/A'),
            ISNULL(c.BackupType, 'Unknown'),
            c.LastBackupTime,
            ISNULL(c.AgeMinutes, DATEDIFF(MINUTE, c.LastBackupTime, GETDATE())),
            ISNULL(c.RPOHours, 24) * 60,  -- Default 24 hours
            ISNULL(c.Status, 'Unknown'),
            GETDATE(),
            ISNULL(c.BackupSource, 'Veeam'),
            c.VmName,
            c.NextExpectedBackupTime,
            c.JobName,
            c.BackupMode,
            c.BackupSizeGB,
            c.Repository,
            c.RepositoryVolume,
            c.RepoTotalTB,
            c.RepoFreeTB,
            c.ExpectedFrequency,
            ISNULL(c.IsCompliant, 0),
            CAST(GETDATE() AS DATE),
            DATEPART(HOUR, GETDATE()),
            CONVERT(INT, CONVERT(VARCHAR(8), GETDATE(), 112)),
            ISNULL(c.ComplianceScore, 0),
            c.RepoFreePercent
        FROM ctl.BackupCompliance c
        WHERE NOT EXISTS (
            SELECT 1 FROM fact.BackupCompliance f
            WHERE f.ServerName = c.ServerName
              AND f.SnapshotDate = CAST(GETDATE() AS DATE)
              AND f.SnapshotHour = DATEPART(HOUR, GETDATE())
        );
        
        PRINT 'Loaded ' + CAST(@@ROWCOUNT AS VARCHAR) + ' records to fact.BackupCompliance';
        
    END TRY
    BEGIN CATCH
        INSERT INTO ctl.ETLErrorLog (ProcedureName, ErrorMessage, ErrorLine, LogDate)
        VALUES ('ctl.usp_Load_BackupCompliance_Safe', ERROR_MESSAGE(), ERROR_LINE(), GETDATE());
        
        THROW;
    END CATCH
END;

GO

-- [ctl].[usp_Load_RepositoryStatus]

CREATE PROCEDURE ctl.usp_Load_RepositoryStatus
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @DbSizeMB INT;
    SELECT @DbSizeMB = SUM(size) * 8 / 1024 FROM sys.database_files;
    
    INSERT INTO ctl.RepositoryStatus (MetricName, MetricValue, CollectedAt)
    VALUES 
        ('DatabaseSizeMB', CAST(@DbSizeMB AS NVARCHAR(256)), GETDATE()),
        ('LastRefresh', CONVERT(NVARCHAR(256), GETDATE(), 120), GETDATE());
    
    PRINT 'Status loaded: ' + CAST(@DbSizeMB AS VARCHAR) + ' MB';
END

GO

-- [ctl].[usp_Maintain_DiskSpaceMetrics]

CREATE   PROCEDURE ctl.usp_Maintain_DiskSpaceMetrics
(
    @RetentionDays INT = 90,
    @SendAlertOnIssue BIT = 1
)
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @DeletedRows INT = 0;
    DECLARE @OrphanedRows INT = 0;
    DECLARE @LowSpaceCount INT = 0;
    
    -- 1. Delete old data beyond retention period
    DELETE FROM fact.DiskSpaceMetrics
    WHERE CaptureDate < DATEADD(DAY, -@RetentionDays, GETDATE());
    
    SET @DeletedRows = @@ROWCOUNT;
    
    -- 2. Remove orphaned records (servers no longer in CMS - source of truth)
    DELETE m
    FROM fact.DiskSpaceMetrics m
    WHERE NOT EXISTS (
        SELECT 1 
        FROM msdb.dbo.sysmanagement_shared_registered_servers_internal cms
        WHERE UPPER(cms.server_name) = UPPER(m.ServerName)
    );
    
    SET @OrphanedRows = @@ROWCOUNT;
    
    -- 3. Check for low disk space alerts (only latest capture per server/drive)
    ;WITH LatestMetrics AS (
        SELECT 
            ServerName,
            DriveLetter,
            FreeSpaceGB,
            FreePercent,
            ROW_NUMBER() OVER (PARTITION BY ServerName, DriveLetter ORDER BY CaptureDate DESC) AS rn
        FROM fact.DiskSpaceMetrics
    )
    SELECT @LowSpaceCount = COUNT(*)
    FROM LatestMetrics lm
    INNER JOIN msdb.dbo.sysmanagement_shared_registered_servers_internal cms
        ON UPPER(cms.server_name) = UPPER(lm.ServerName)
    WHERE lm.rn = 1
    AND (lm.FreePercent < 10 OR lm.FreeSpaceGB < 5);  -- Less than 10% or less than 5GB
    
    -- 4. Log maintenance results
    PRINT 'DBAOps | JB: DiskSpace Metrics Maintenance completed';
    PRINT '  - Deleted ' + CAST(@DeletedRows AS VARCHAR(10)) + ' rows older than ' + CAST(@RetentionDays AS VARCHAR(10)) + ' days';
    PRINT '  - Removed ' + CAST(@OrphanedRows AS VARCHAR(10)) + ' orphaned server records';
    PRINT '  - Low space alerts: ' + CAST(@LowSpaceCount AS VARCHAR(10));
    
    -- 5. Send alert only if there are low disk space issues
    IF @SendAlertOnIssue = 1 AND @LowSpaceCount > 0
    BEGIN
        DECLARE @Subject NVARCHAR(255) = N'DBAOps | JB | ALERT: Low Disk Space Detected on ' + CAST(@LowSpaceCount AS NVARCHAR(10)) + ' drives';
        DECLARE @Body NVARCHAR(MAX);
        
        SET @Body = N'<html><head><style>
            body { font-family: Segoe UI, Arial, sans-serif; font-size: 12px; }
            table { border-collapse: collapse; margin: 10px 0; }
            th { background-color: #C0392B; color: white; padding: 6px 10px; }
            td { padding: 4px 10px; border: 1px solid #ddd; }
            .critical { background-color: #FADBD8; }
        </style></head><body>';
        
        SET @Body = @Body + N'<h2>DBAOps | JB | Low Disk Space Alert</h2>';
        SET @Body = @Body + N'<p><b>Generated:</b> ' + CONVERT(NVARCHAR(30), GETDATE(), 120) + N'</p>';
        SET @Body = @Body + N'<table><tr><th>Server</th><th>Drive</th><th>Free GB</th><th>Free %</th></tr>';
        
        ;WITH LatestMetrics AS (
            SELECT 
                ServerName,
                DriveLetter,
                FreeSpaceGB,
                FreePercent,
                ROW_NUMBER() OVER (PARTITION BY ServerName, DriveLetter ORDER BY CaptureDate DESC) AS rn
            FROM fact.DiskSpaceMetrics
        )
        SELECT @Body = @Body + 
            N'<tr class="critical"><td>' + lm.ServerName + 
            N'</td><td>' + lm.DriveLetter + 
            N'</td><td>' + CAST(CAST(lm.FreeSpaceGB AS DECIMAL(10,2)) AS NVARCHAR(20)) + 
            N'</td><td>' + CAST(CAST(lm.FreePercent AS DECIMAL(5,2)) AS NVARCHAR(10)) + N'%' +
            N'</td></tr>'
        FROM LatestMetrics lm
        INNER JOIN msdb.dbo.sysmanagement_shared_registered_servers_internal cms
            ON UPPER(cms.server_name) = UPPER(lm.ServerName)
        WHERE lm.rn = 1
        AND (lm.FreePercent < 10 OR lm.FreeSpaceGB < 5)
        ORDER BY lm.FreePercent;
        
        SET @Body = @Body + N'</table></body></html>';
        
        -- Get mail profile
        DECLARE @ProfileName SYSNAME;
        SELECT TOP 1 @ProfileName = name FROM msdb.dbo.sysmail_profile ORDER BY name;
        
        EXEC msdb.dbo.sp_send_dbmail
            @profile_name = @ProfileName,
            @recipients = 'DBAOps@YOURORG.gov;SQLServerAdminNotification@YOURORG.gov',
            @subject = @Subject,
            @body = @Body,
            @body_format = 'HTML';
            
        PRINT '  - Alert email sent';
    END
    ELSE IF @LowSpaceCount = 0
    BEGIN
        PRINT '  - No disk space issues detected. No alert sent.';
    END
END;

GO

-- [ctl].[usp_Master_DataRetention]

CREATE PROCEDURE ctl.usp_Master_DataRetention
    @Debug BIT = 0
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @SQL NVARCHAR(MAX);
    DECLARE @DateCol NVARCHAR(128);
    DECLARE @RowsDeleted INT;
    DECLARE @StartTime DATETIME = GETDATE();
    
    PRINT 'â•”â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•—'
    PRINT 'â•‘   DBAOps Master Data Retention Cleanup                                        â•‘'
    PRINT 'â•‘   Started: ' + CONVERT(VARCHAR, @StartTime, 120) + '                                         â•‘'
    PRINT 'â•šâ•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•'
    PRINT ''
    
    -- Retention Policy Table
    CREATE TABLE #RetentionPolicy (
        TableSchema NVARCHAR(128),
        TableName NVARCHAR(128),
        RetentionDays INT,
        Priority INT
    );
    
    -- Define retention policies
    INSERT INTO #RetentionPolicy VALUES
    ('fact', 'BackupCompliance', 7, 1),
    ('ctl', 'BackupCompliance', 7, 2),
    ('fact', 'ServerHealth', 14, 3),
    ('fact', 'MemoryCompliance', 14, 4),
    ('metrics', 'FACT_ServerHealth', 14, 5),
    ('ctl', 'VeeamBackups', 30, 6),
    ('audit', 'AuditLog', 30, 7),
    ('audit', 'ObjectChanges', 30, 8),
    ('audit', 'LoginFailures', 90, 9),
    ('audit', 'PermissionChanges', 90, 10),
    ('dbo', 'fact_Alerts', 30, 11),
    ('dbo', 'fact_WaitStats', 14, 12),
    ('fact', 'DiskSpaceMetrics', 30, 13),
    ('fact', 'PerformanceSnapshots', 14, 14),
    ('fact', 'ReplicationMetricsHistory', 30, 15);
    
    DECLARE @Schema NVARCHAR(128), @Table NVARCHAR(128), @Days INT;
    
    DECLARE retention_cursor CURSOR FOR
    SELECT TableSchema, TableName, RetentionDays
    FROM #RetentionPolicy
    ORDER BY Priority;
    
    OPEN retention_cursor;
    FETCH NEXT FROM retention_cursor INTO @Schema, @Table, @Days;
    
    WHILE @@FETCH_STATUS = 0
    BEGIN
        -- Check if table exists
        IF OBJECT_ID(QUOTENAME(@Schema) + '.' + QUOTENAME(@Table), 'U') IS NOT NULL
        BEGIN
            -- Find date column
            SELECT TOP 1 @DateCol = COLUMN_NAME
            FROM INFORMATION_SCHEMA.COLUMNS
            WHERE TABLE_SCHEMA = @Schema AND TABLE_NAME = @Table
            AND DATA_TYPE IN ('datetime', 'datetime2', 'date')
            ORDER BY ORDINAL_POSITION;
            
            IF @DateCol IS NOT NULL
            BEGIN
                SET @SQL = 'DELETE FROM ' + QUOTENAME(@Schema) + '.' + QUOTENAME(@Table) + 
                           ' WHERE ' + QUOTENAME(@DateCol) + ' < DATEADD(DAY, -' + CAST(@Days AS VARCHAR) + ', GETDATE())';
                
                IF @Debug = 1
                    PRINT @SQL;
                ELSE
                BEGIN
                    EXEC sp_executesql @SQL;
                    SET @RowsDeleted = @@ROWCOUNT;
                    IF @RowsDeleted > 0
                        PRINT '  ' + @Schema + '.' + @Table + ': Deleted ' + CAST(@RowsDeleted AS VARCHAR) + ' rows (>' + CAST(@Days AS VARCHAR) + ' days)';
                END
            END
        END
        
        FETCH NEXT FROM retention_cursor INTO @Schema, @Table, @Days;
    END
    
    CLOSE retention_cursor;
    DEALLOCATE retention_cursor;
    
    DROP TABLE #RetentionPolicy;
    
    PRINT ''
    PRINT 'Data Retention Cleanup Complete - Duration: ' + 
          CAST(DATEDIFF(SECOND, @StartTime, GETDATE()) AS VARCHAR) + ' seconds';
END

GO

-- [ctl].[usp_MasterDataCollection]

-- ============================================================================
-- STEP 1: Create Master Data Collection Procedure
-- ============================================================================
CREATE   PROCEDURE ctl.usp_MasterDataCollection
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @StartTime DATETIME = GETDATE();
    DECLARE @ErrorCount INT = 0;
    DECLARE @SuccessCount INT = 0;
    
    PRINT '=== DBAOps Master Data Collection Started: ' + CONVERT(VARCHAR, @StartTime, 120) + ' ===';
    
    -- 1. Collect Disk Space
    BEGIN TRY
        PRINT 'Collecting Disk Space...';
        EXEC ctl.usp_Collect_DiskSpace;
        SET @SuccessCount += 1;
        PRINT '  [OK] Disk Space collected';
    END TRY
    BEGIN CATCH
        SET @ErrorCount += 1;
        PRINT '  [ERROR] Disk Space: ' + ERROR_MESSAGE();
    END CATCH
    
    -- 2. Collect Memory Compliance
    BEGIN TRY
        PRINT 'Collecting Memory Compliance...';
        EXEC ctl.usp_Collect_MemoryCompliance;
        SET @SuccessCount += 1;
        PRINT '  [OK] Memory Compliance collected';
    END TRY
    BEGIN CATCH
        SET @ErrorCount += 1;
        PRINT '  [ERROR] Memory Compliance: ' + ERROR_MESSAGE();
    END CATCH
    
    -- 3. Collect Performance Compliance
    BEGIN TRY
        PRINT 'Collecting Performance Compliance...';
        EXEC ctl.usp_Collect_PerformanceCompliance;
        SET @SuccessCount += 1;
        PRINT '  [OK] Performance Compliance collected';
    END TRY
    BEGIN CATCH
        SET @ErrorCount += 1;
        PRINT '  [ERROR] Performance: ' + ERROR_MESSAGE();
    END CATCH
    
    -- 4. Collect Server Health
    BEGIN TRY
        PRINT 'Collecting Server Health...';
        EXEC ctl.usp_Collect_ServerHealth;
        SET @SuccessCount += 1;
        PRINT '  [OK] Server Health collected';
    END TRY
    BEGIN CATCH
        SET @ErrorCount += 1;
        PRINT '  [ERROR] Server Health: ' + ERROR_MESSAGE();
    END CATCH
    
    -- 5. Collect Server Config Compliance (MAXDOP, Cost Threshold)
    BEGIN TRY
        PRINT 'Collecting Server Config Compliance...';
        EXEC dbo.usp_CollectServerConfigCompliance;
        SET @SuccessCount += 1;
        PRINT '  [OK] Config Compliance collected';
    END TRY
    BEGIN CATCH
        SET @ErrorCount += 1;
        PRINT '  [ERROR] Config Compliance: ' + ERROR_MESSAGE();
    END CATCH
    
    -- 6. Update meta.Collectors last run time
    UPDATE meta.Collectors SET FrequencyMinutes = 60;
    
    -- Summary
    PRINT '';
    PRINT '=== Collection Complete ===';
    PRINT 'End Time: ' + CONVERT(VARCHAR, GETDATE(), 120);
    PRINT 'Duration: ' + CAST(DATEDIFF(SECOND, @StartTime, GETDATE()) AS VARCHAR) + ' seconds';
    PRINT 'Success: ' + CAST(@SuccessCount AS VARCHAR) + ' | Errors: ' + CAST(@ErrorCount AS VARCHAR);
    
    IF @ErrorCount > 0
        RAISERROR('Collection completed with %d error(s)', 10, 1, @ErrorCount);
END;

GO

-- [ctl].[usp_Recalculate_BackupCompliance]
CREATE OR ALTER PROCEDURE ctl.usp_Recalculate_BackupCompliance  
AS  
BEGIN  
    SET NOCOUNT ON;  
      
    DECLARE @Now DATETIME = GETDATE();
    DECLARE @SevenDaysAgo DATETIME = DATEADD(DAY, -7, @Now);
    DECLARE @Updated INT;

    -- First, calculate backup count in last 7 days per server
    ;WITH BackupCounts AS (
        SELECT 
            ServerName,
            COUNT(*) AS BackupsInLast7Days,
            MAX(LastBackupTime) AS LastBackupTime
        FROM ctl.BackupCompliance
        WHERE LastBackupTime >= @SevenDaysAgo
        GROUP BY ServerName
    )
    
    UPDATE c  
    SET   
        IsCompliant = CASE   
            -- PROD: Must have 2+ backups in 7 days
            WHEN s.Environment = 'PROD'
             AND ISNULL(bc.BackupsInLast7Days, 0) >= 2 THEN 1
            -- NON-PROD: Must have 1+ backup in 7 days
            WHEN s.Environment IN ('NONPROD', 'DEV', 'TEST', 'UAT', 'NON-PROD', 'Development', 'Testing')
             AND ISNULL(bc.BackupsInLast7Days, 0) >= 1 THEN 1
            ELSE 0  
        END,  
          
        ComplianceScore = CASE   
            -- PROD scoring (based on 2 backups per week requirement)
            WHEN s.Environment = 'PROD' THEN  
                CASE   
                    WHEN ISNULL(bc.BackupsInLast7Days, 0) >= 2 AND DATEDIFF(HOUR, c.LastBackupTime, @Now) <= 84 THEN 100
                    WHEN ISNULL(bc.BackupsInLast7Days, 0) >= 2 THEN 90
                    WHEN ISNULL(bc.BackupsInLast7Days, 0) = 1 AND DATEDIFF(HOUR, c.LastBackupTime, @Now) <= 84 THEN 70
                    WHEN ISNULL(bc.BackupsInLast7Days, 0) = 1 THEN 50
                    WHEN DATEDIFF(HOUR, c.LastBackupTime, @Now) <= 168 THEN 30
                    ELSE 10  
                END  
            -- NON-PROD scoring (1 backup per week requirement)
            WHEN s.Environment IN ('NONPROD', 'DEV', 'TEST', 'UAT', 'NON-PROD', 'Development', 'Testing') THEN  
                CASE   
                    WHEN ISNULL(bc.BackupsInLast7Days, 0) >= 1 AND DATEDIFF(HOUR, c.LastBackupTime, @Now) <= 72 THEN 100
                    WHEN ISNULL(bc.BackupsInLast7Days, 0) >= 1 AND DATEDIFF(HOUR, c.LastBackupTime, @Now) <= 168 THEN 90
                    WHEN DATEDIFF(HOUR, c.LastBackupTime, @Now) <= 240 THEN 50
                    ELSE 30  
                END  
            ELSE 50
        END,  
          
        Status = CASE   
            WHEN c.LastBackupTime IS NULL THEN 'Missing'
            -- PROD: 2 backups in 7 days = Success
            WHEN s.Environment = 'PROD' THEN  
                CASE   
                    WHEN ISNULL(bc.BackupsInLast7Days, 0) >= 2 THEN 'Success'
                    WHEN ISNULL(bc.BackupsInLast7Days, 0) = 1 THEN 'Stale'
                    ELSE 'Critical'  
                END  
            -- NON-PROD: 1 backup in 7 days = Success
            WHEN s.Environment IN ('NONPROD', 'DEV', 'TEST', 'UAT', 'NON-PROD', 'Development', 'Testing') THEN  
                CASE   
                    WHEN ISNULL(bc.BackupsInLast7Days, 0) >= 1 THEN 'Success'
                    WHEN DATEDIFF(HOUR, c.LastBackupTime, @Now) <= 336 THEN 'Stale'
                    ELSE 'Critical'  
                END  
            ELSE 'Unknown'
        END  
          
    FROM ctl.BackupCompliance c  
    LEFT JOIN dim.[Server] s ON c.ServerName = s.ServerName
    LEFT JOIN BackupCounts bc ON c.ServerName = bc.ServerName;  
      
    SET @Updated = @@ROWCOUNT;  
    PRINT 'Recalculated compliance for ' + CAST(@Updated AS VARCHAR) + ' records';  
      
    SELECT   
        ISNULL(s.Environment, 'Unknown') AS Environment,  
        c.Status,  
        COUNT(*) AS RecordCount,  
        AVG(CAST(c.ComplianceScore AS DECIMAL(5,2))) AS AvgScore  
    FROM ctl.BackupCompliance c  
    LEFT JOIN dim.[Server] s ON c.ServerName = s.ServerName  
    GROUP BY s.Environment, c.Status  
    ORDER BY s.Environment, c.Status;  
END;

GO

-- [ctl].[usp_SendDailyHealthDigest]

CREATE PROCEDURE ctl.usp_SendDailyHealthDigest
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @html NVARCHAR(MAX);
    DECLARE @Subject NVARCHAR(255);
    DECLARE @TotalServers INT = 0;
    DECLARE @UpdatedLast24H INT = 0;
    DECLARE @StaleServers INT = 0;
    DECLARE @NonCompliant INT = 0;
    DECLARE @Unknown INT = 0;
    DECLARE @HasIssues BIT = 0;
    
    -- Get metrics from CMS (source of truth)
    SELECT @TotalServers = COUNT(DISTINCT ServerName)
    FROM config.vw_MonitoredServers;
    
    -- Get health metrics
    SELECT 
        @UpdatedLast24H = SUM(CASE WHEN si.LastSeen >= DATEADD(HOUR, -24, GETDATE()) THEN 1 ELSE 0 END),
        @StaleServers = SUM(CASE WHEN si.LastSeen < DATEADD(HOUR, -24, GETDATE()) OR si.LastSeen IS NULL THEN 1 ELSE 0 END),
        @NonCompliant = SUM(CASE WHEN si.MaxDOP < 4 OR si.CostThreshold < 35 THEN 1 ELSE 0 END),
        @Unknown = SUM(CASE WHEN si.ServerName IS NULL THEN 1 ELSE 0 END)
    FROM config.vw_MonitoredServers cms
    LEFT JOIN config.ServerInventory si 
        ON UPPER(cms.ServerName) = UPPER(si.ServerName)
        AND si.IsActive = 1;
    
    -- Determine if there are issues
    IF @StaleServers > 0 OR @NonCompliant > 0 OR @Unknown > 0
        SET @HasIssues = 1;
    
    -- =====================================================
    -- SMART EMAIL LOGIC: Only send if issues exist
    -- =====================================================
    IF @HasIssues = 0
    BEGIN
        PRINT 'DBAOps | JB: All ' + CAST(@TotalServers AS VARCHAR(10)) + 
            ' servers healthy and reporting. No email sent.';
        RETURN;
    END;
    
    -- Build HTML with issues highlighted
    SET @Subject = '*** DBAOps Daily Health Digest - ' +
        CASE 
            WHEN @NonCompliant > 0 OR @Unknown > 0 THEN 'ACTION REQUIRED'
            WHEN @StaleServers > 0 THEN 'STALE DATA WARNING'
            ELSE 'REVIEW'
        END + ' ***';
    
    SET @html = '
    <html><head><style>
        body { font-family: Segoe UI, Arial, sans-serif; }
        table { border-collapse: collapse; margin: 15px 0; }
        th { background-color: #1a1a2e; color: white; padding: 10px; text-align: left; }
        td { padding: 8px 15px; border-bottom: 1px solid #ddd; }
        .good { color: green; font-weight: bold; }
        .warning { color: orange; font-weight: bold; }
        .critical { color: red; font-weight: bold; }
        .summary-box { background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 20px; }
    </style></head><body>
    <h2>DBAOps Daily Health Digest</h2>
    <div class="summary-box">
        <strong>Generated:</strong> ' + CONVERT(VARCHAR(30), GETDATE(), 120) + '<br/>
        <strong>Source:</strong> CMS (' + CAST(@TotalServers AS VARCHAR(10)) + ' monitored servers)
    </div>
    <table>
        <tr><th>Metric</th><th>Value</th><th>Status</th></tr>
        <tr>
            <td>Total CMS Servers</td>
            <td>' + CAST(@TotalServers AS VARCHAR(10)) + '</td>
            <td class="good">âœ“</td>
        </tr>
        <tr>
            <td>Updated Last 24H</td>
            <td>' + CAST(@UpdatedLast24H AS VARCHAR(10)) + '</td>
            <td class="' + CASE WHEN @UpdatedLast24H = @TotalServers THEN 'good' ELSE 'warning' END + '">' +
            CASE WHEN @UpdatedLast24H = @TotalServers THEN 'âœ“' ELSE 'âš ' END + '</td>
        </tr>
        <tr>
            <td>Stale (>24H)</td>
            <td>' + CAST(@StaleServers AS VARCHAR(10)) + '</td>
            <td class="' + CASE WHEN @StaleServers = 0 THEN 'good' ELSE 'warning' END + '">' +
            CASE WHEN @StaleServers = 0 THEN 'âœ“' ELSE 'âš  Review' END + '</td>
        </tr>
        <tr>
            <td>Non-Compliant (MaxDOP/CTFP)</td>
            <td>' + CAST(@NonCompliant AS VARCHAR(10)) + '</td>
            <td class="' + CASE WHEN @NonCompliant = 0 THEN 'good' ELSE 'critical' END + '">' +
            CASE WHEN @NonCompliant = 0 THEN 'âœ“' ELSE 'âœ— Fix Required' END + '</td>
        </tr>
        <tr>
            <td>Unknown/Missing Data</td>
            <td>' + CAST(@Unknown AS VARCHAR(10)) + '</td>
            <td class="' + CASE WHEN @Unknown = 0 THEN 'good' ELSE 'warning' END + '">' +
            CASE WHEN @Unknown = 0 THEN 'âœ“' ELSE 'âš  Investigate' END + '</td>
        </tr>
    </table>';
    
    -- Add stale server details if any
    IF @StaleServers > 0
    BEGIN
        SET @html = @html + '<h3>Stale Servers (No update in 24+ hours)</h3><table>
            <tr><th>Server</th><th>Last Seen</th><th>Days Stale</th></tr>';
        
        SELECT @html = @html + 
            '<tr><td>' + cms.ServerName + '</td>' +
            '<td>' + ISNULL(CONVERT(VARCHAR(20), si.LastSeen, 120), 'Never') + '</td>' +
            '<td>' + CAST(ISNULL(DATEDIFF(DAY, si.LastSeen, GETDATE()), 999) AS VARCHAR(10)) + '</td></tr>'
        FROM config.vw_MonitoredServers cms
        LEFT JOIN config.ServerInventory si 
            ON UPPER(cms.ServerName) = UPPER(si.ServerName)
        WHERE si.LastSeen < DATEADD(HOUR, -24, GETDATE()) OR si.LastSeen IS NULL
        ORDER BY ISNULL(si.LastSeen, '1900-01-01');
        
        SET @html = @html + '</table>';
    END;
    
    SET @html = @html + '
    <p style="margin-top:20px;font-size:12px;color:#666;">
        This report only includes items requiring attention. Full status available in dashboard.
    </p>
    </body></html>';
    
    -- Send email
    EXEC msdb.dbo.sp_send_dbmail
        @profile_name = 'DBAOpsProfile',
        @recipients = 'SQLServerAdminNotification@YOURORG.gov',
        @subject = @Subject,
        @body = @html,
        @body_format = 'HTML';
    
    PRINT 'DBAOps | JB: Daily Health Digest sent with ' + 
        CAST(@StaleServers AS VARCHAR(10)) + ' stale, ' +
        CAST(@NonCompliant AS VARCHAR(10)) + ' non-compliant.';
END;

GO

-- [ctl].[usp_SendNonComplianceAlert]

CREATE PROCEDURE ctl.usp_SendNonComplianceAlert
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @html NVARCHAR(MAX);
    DECLARE @NonCompliantCount INT = 0;
    DECLARE @UnknownCount INT = 0;
    DECLARE @Subject NVARCHAR(255);
    
    -- Build results from CMS (source of truth)
    CREATE TABLE #ComplianceResults (
        ServerName NVARCHAR(256),
        MaxDOP INT,
        CostThreshold INT,
        ComplianceStatus NVARCHAR(20)
    );
    
    INSERT INTO #ComplianceResults
    SELECT 
        cms.ServerName,
        ISNULL(si.MaxDOP, -1) AS MaxDOP,
        ISNULL(si.CostThreshold, -1) AS CostThreshold,
        CASE 
            WHEN si.ServerName IS NULL THEN 'UNKNOWN'
            WHEN si.MaxDOP IS NULL OR si.CostThreshold IS NULL THEN 'UNKNOWN'
            WHEN si.MaxDOP < 4 OR si.CostThreshold < 35 THEN 'NON-COMPLIANT'
            ELSE 'COMPLIANT'
        END AS ComplianceStatus
    FROM config.vw_MonitoredServers cms
    LEFT JOIN config.ServerInventory si 
        ON UPPER(cms.ServerName) = UPPER(si.ServerName)
        AND si.IsActive = 1;
    
    -- Count issues
    SELECT 
        @NonCompliantCount = SUM(CASE WHEN ComplianceStatus = 'NON-COMPLIANT' THEN 1 ELSE 0 END),
        @UnknownCount = SUM(CASE WHEN ComplianceStatus = 'UNKNOWN' THEN 1 ELSE 0 END)
    FROM #ComplianceResults;
    
    -- =====================================================
    -- SMART EMAIL LOGIC: Only send if issues exist
    -- =====================================================
    IF @NonCompliantCount = 0 AND @UnknownCount = 0
    BEGIN
        PRINT 'DBAOps | JB: All servers compliant (MaxDOP >= 4, CTFP >= 35). No email sent.';
        DROP TABLE #ComplianceResults;
        RETURN;
    END;
    
    -- Build HTML for non-compliant/unknown servers only
    SET @Subject = '*** DBAOps ALERT: ' + 
        CAST(@NonCompliantCount AS VARCHAR(10)) + ' Non-Compliant, ' +
        CAST(@UnknownCount AS VARCHAR(10)) + ' Unknown - MaxDOP/CTFP ***';
    
    SET @html = '
    <html><head><style>
        body { font-family: Segoe UI, Arial, sans-serif; }
        table { border-collapse: collapse; width: 100%; }
        th { background-color: #1a1a2e; color: white; padding: 10px; text-align: left; }
        td { padding: 8px; border-bottom: 1px solid #ddd; }
        .non-compliant { background-color: #f8d7da; }
        .unknown { background-color: #fff3cd; }
        .summary { background-color: #f8f9fa; padding: 15px; margin-bottom: 20px; border-radius: 8px; }
    </style></head><body>
    <h2>DBAOps Configuration Compliance Alert</h2>
    <div class="summary">
        <strong>Summary:</strong> ' + CAST(@NonCompliantCount AS VARCHAR(10)) + ' Non-Compliant, ' +
        CAST(@UnknownCount AS VARCHAR(10)) + ' Unknown<br/>
        <strong>Generated:</strong> ' + CONVERT(VARCHAR(30), GETDATE(), 120) + '<br/>
        <strong>Source:</strong> CMS (Central Management Server)
    </div>
    <p>The following servers require attention:</p>
    <table>
        <tr>
            <th>Server</th>
            <th>MaxDOP</th>
            <th>Cost Threshold</th>
            <th>Status</th>
        </tr>';
    
    -- Add non-compliant servers
    SELECT @html = @html + 
        '<tr class="non-compliant"><td>' + ServerName + '</td>' +
        '<td>' + CASE WHEN MaxDOP = -1 THEN 'N/A' ELSE CAST(MaxDOP AS VARCHAR(10)) END + '</td>' +
        '<td>' + CASE WHEN CostThreshold = -1 THEN 'N/A' ELSE CAST(CostThreshold AS VARCHAR(10)) END + '</td>' +
        '<td style="color:red;font-weight:bold;">NON-COMPLIANT</td></tr>'
    FROM #ComplianceResults
    WHERE ComplianceStatus = 'NON-COMPLIANT'
    ORDER BY ServerName;
    
    -- Add unknown servers
    SELECT @html = @html + 
        '<tr class="unknown"><td>' + ServerName + '</td>' +
        '<td>' + CASE WHEN MaxDOP = -1 THEN 'N/A' ELSE CAST(MaxDOP AS VARCHAR(10)) END + '</td>' +
        '<td>' + CASE WHEN CostThreshold = -1 THEN 'N/A' ELSE CAST(CostThreshold AS VARCHAR(10)) END + '</td>' +
        '<td style="color:orange;font-weight:bold;">UNKNOWN</td></tr>'
    FROM #ComplianceResults
    WHERE ComplianceStatus = 'UNKNOWN'
    ORDER BY ServerName;
    
    SET @html = @html + '</table>
    <p style="margin-top:20px;font-size:12px;color:#666;">
        <strong>Compliance Rules:</strong> MaxDOP >= 4, Cost Threshold >= 35<br/>
        This report only includes servers requiring attention. Dashboard shows full status.
    </p>
    </body></html>';
    
    -- Send email
    EXEC msdb.dbo.sp_send_dbmail
        @profile_name = 'DBAOpsProfile',
        @recipients = 'SQLServerAdminNotification@YOURORG.gov',
        @subject = @Subject,
        @body = @html,
        @body_format = 'HTML';
    
    PRINT 'DBAOps | JB: Non-compliance alert sent. ' + 
        CAST(@NonCompliantCount AS VARCHAR(10)) + ' non-compliant, ' +
        CAST(@UnknownCount AS VARCHAR(10)) + ' unknown.';
    
    DROP TABLE #ComplianceResults;
END;

GO

-- [ctl].[usp_SendServerInventoryReport]

CREATE PROCEDURE ctl.usp_SendServerInventoryReport
/*
================================================================================
   DBAOps Monitoring and Compliance Framework
   Author: John Bosco RHOYLLE INNOVATION EMPIRE™, Sr SQL DBA/DBAOps Engineer
   Procedure: ctl.usp_SendServerInventoryReport
   
   UPDATED: 2026-01-27 - Use CMS, only email on changes/issues
================================================================================
*/
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @ProfileName NVARCHAR(128);
    DECLARE @Subject NVARCHAR(255);
    DECLARE @Body NVARCHAR(MAX);
    DECLARE @CMSCount INT = 0;
    DECLARE @InventoryCount INT = 0;
    DECLARE @MissingFromInventory INT = 0;
    DECLARE @StaleServers INT = 0;
    DECLARE @JobCount INT = 0;
    DECLARE @FailedJobs INT = 0;
    DECLARE @HasIssues BIT = 0;
    
    -- Get counts from CMS (source of truth)
    SELECT @CMSCount = COUNT(DISTINCT ServerName) 
    FROM config.vw_MonitoredServers;
    
    -- Get inventory count
    SELECT @InventoryCount = COUNT(*) 
    FROM config.ServerInventory 
    WHERE IsActive = 1;
    
    -- Find servers in CMS but missing from inventory
    SELECT @MissingFromInventory = COUNT(*)
    FROM config.vw_MonitoredServers cms
    WHERE NOT EXISTS (
        SELECT 1 FROM config.ServerInventory si 
        WHERE UPPER(si.ServerName) = UPPER(cms.ServerName) AND si.IsActive = 1
    );
    
    -- Find stale servers (no update in 7 days)
    SELECT @StaleServers = COUNT(*)
    FROM config.ServerInventory 
    WHERE IsActive = 1 
    AND (LastSeen < DATEADD(DAY, -7, GETDATE()) OR LastSeen IS NULL);
    
    -- Get job counts
    SELECT @JobCount = COUNT(*) FROM msdb.dbo.sysjobs WHERE name LIKE 'DBAOps%';
    
    -- Get failed jobs in last 24h
    SELECT @FailedJobs = COUNT(DISTINCT j.name)
    FROM msdb.dbo.sysjobhistory h
    JOIN msdb.dbo.sysjobs j ON h.job_id = j.job_id
    WHERE j.name LIKE 'DBAOps%'
    AND h.run_status = 0
    AND h.step_id = 0
    AND CONVERT(DATE, CAST(h.run_date AS VARCHAR(8)), 112) >= DATEADD(DAY, -1, CAST(GETDATE() AS DATE));
    
    -- Determine if issues exist
    IF @MissingFromInventory > 0 OR @StaleServers > 5 OR @FailedJobs > 0
        SET @HasIssues = 1;
    
    -- =====================================================
    -- SMART EMAIL LOGIC: Only send if issues exist
    -- =====================================================
    IF @HasIssues = 0
    BEGIN
        PRINT 'DBAOps | JB: Server inventory healthy. ' + 
            CAST(@CMSCount AS VARCHAR(10)) + ' CMS servers, ' +
            CAST(@JobCount AS VARCHAR(10)) + ' jobs OK. No email sent.';
        RETURN;
    END;
    
    -- Build report
    SET @Subject = '*** DBAOps Weekly SQL Server Inventory Report - ' +
        CASE 
            WHEN @FailedJobs > 0 THEN 'FAILED JOBS DETECTED'
            WHEN @MissingFromInventory > 0 THEN 'SYNC REQUIRED'
            ELSE 'REVIEW REQUIRED'
        END + ' ***';
    
    SET @Body = '=================================================================================' + CHAR(13) + CHAR(10) +
                '  DBAOps Weekly SQL Server Inventory Report' + CHAR(13) + CHAR(10) +
                '  Generated: ' + CONVERT(VARCHAR, GETDATE(), 120) + CHAR(13) + CHAR(10) +
                '  Server: ' + @@SERVERNAME + CHAR(13) + CHAR(10) +
                '  Source: CMS (Central Management Server)' + CHAR(13) + CHAR(10) +
                '=================================================================================' + CHAR(13) + CHAR(10) + CHAR(13) + CHAR(10) +
                'SUMMARY' + CHAR(13) + CHAR(10) +
                '---------' + CHAR(13) + CHAR(10) +
                '  CMS Servers: ' + CAST(@CMSCount AS VARCHAR) + CHAR(13) + CHAR(10) +
                '  Inventory Active: ' + CAST(@InventoryCount AS VARCHAR) + CHAR(13) + CHAR(10) +
                '  Missing from Inventory: ' + CAST(@MissingFromInventory AS VARCHAR) + 
                    CASE WHEN @MissingFromInventory > 0 THEN ' *** ACTION REQUIRED ***' ELSE '' END + CHAR(13) + CHAR(10) +
                '  Stale (>7 days): ' + CAST(@StaleServers AS VARCHAR) + CHAR(13) + CHAR(10) +
                '  DBAOps Jobs: ' + CAST(@JobCount AS VARCHAR) + CHAR(13) + CHAR(10) +
                '  Failed Jobs (24h): ' + CAST(@FailedJobs AS VARCHAR) + 
                    CASE WHEN @FailedJobs > 0 THEN ' *** INVESTIGATE ***' ELSE '' END + CHAR(13) + CHAR(10) + CHAR(13) + CHAR(10);
    
    -- Add missing servers if any
    IF @MissingFromInventory > 0
    BEGIN
        SET @Body = @Body + 'SERVERS MISSING FROM INVENTORY' + CHAR(13) + CHAR(10) +
                    '--------------------------------' + CHAR(13) + CHAR(10);
        
        SELECT @Body = @Body + '  - ' + cms.ServerName + CHAR(13) + CHAR(10)
        FROM config.vw_MonitoredServers cms
        WHERE NOT EXISTS (
            SELECT 1 FROM config.ServerInventory si 
            WHERE UPPER(si.ServerName) = UPPER(cms.ServerName) AND si.IsActive = 1
        );
        
        SET @Body = @Body + CHAR(13) + CHAR(10) + 
            'Run: EXEC config.usp_SyncServerInventoryFromCMS to sync.' + CHAR(13) + CHAR(10) + CHAR(13) + CHAR(10);
    END;
    
    SET @Body = @Body + '=================================================================================' + CHAR(13) + CHAR(10) +
                '  DBAOps Monitoring and Compliance Framework' + CHAR(13) + CHAR(10) +
                '  Author: John Bosco RHOYLLE INNOVATION EMPIRE™, Sr SQL DBA/DBAOps Engineer' + CHAR(13) + CHAR(10) +
                '  This report only sent when issues detected. Full status in dashboard.' + CHAR(13) + CHAR(10) +
                '=================================================================================';
    
    -- Get mail profile
    SELECT TOP 1 @ProfileName = name FROM msdb.dbo.sysmail_profile;
    
    IF @ProfileName IS NOT NULL
    BEGIN
        EXEC msdb.dbo.sp_send_dbmail
            @profile_name = @ProfileName,
            @recipients = N'DBAOps@YOURORG.gov',
            @subject = @Subject,
            @body = @Body;
        PRINT 'DBAOps | JB: Inventory report sent via: ' + @ProfileName;
    END
    ELSE
        PRINT 'DBAOps | JB: No mail profile available';
END;

GO

-- [ctl].[usp_Upsert_ServerInventory]
CREATE OR ALTER PROCEDURE ctl.usp_Upsert_ServerInventory
(
    @ServerName      NVARCHAR(128),
    @InstanceName    NVARCHAR(128),
    @Environment     NVARCHAR(20),
    @IPAddress       NVARCHAR(50),
    @OSVersion       NVARCHAR(200),
    @SQLVersion      NVARCHAR(100),
    @SQLEdition      NVARCHAR(200),
    @CPUCount        INT,
    @TotalMemoryMB   INT,
    @MaxMemoryMB     INT,
    @LastPatchDate   DATETIME = NULL,
    @MaxDOP          INT = NULL,
    @CostThreshold   INT = NULL
)
AS
BEGIN
    SET NOCOUNT ON;

    MERGE config.ServerInventory AS tgt
    USING (
        SELECT 
            @ServerName AS ServerName,
            @InstanceName AS InstanceName,
            @Environment AS Environment,
            @IPAddress AS IPAddress,
            @OSVersion AS OSVersion,
            @SQLVersion AS SQLVersion,
            @SQLEdition AS SQLEdition,
            @CPUCount AS CPUCount,
            @TotalMemoryMB AS TotalMemoryMB,
            @MaxMemoryMB AS MaxMemoryMB,
            @LastPatchDate AS LastPatchDate,
            @MaxDOP AS MaxDOP,
            @CostThreshold AS CostThreshold,
            GETDATE() AS LastSeen
    ) AS src
    ON tgt.ServerName = src.ServerName
    WHEN MATCHED THEN 
        UPDATE SET 
            InstanceName   = src.InstanceName,
            Environment    = src.Environment,
            IPAddress      = src.IPAddress,
            OSVersion      = src.OSVersion,
            SQLVersion     = src.SQLVersion,
            SQLEdition     = src.SQLEdition,
            CPUCount       = src.CPUCount,
            TotalMemoryMB  = src.TotalMemoryMB,
            MaxMemoryMB    = src.MaxMemoryMB,
            LastPatchDate  = src.LastPatchDate,
            MaxDOP         = src.MaxDOP,
            CostThreshold  = src.CostThreshold,
            LastSeen       = src.LastSeen
    WHEN NOT MATCHED THEN 
        INSERT (ServerName, InstanceName, Environment, IPAddress, OSVersion, SQLVersion,
                SQLEdition, CPUCount, TotalMemoryMB, MaxMemoryMB, LastPatchDate,
                MaxDOP, CostThreshold, LastSeen)
        VALUES (src.ServerName, src.InstanceName, src.Environment, src.IPAddress,
                src.OSVersion, src.SQLVersion, src.SQLEdition, src.CPUCount,
                src.TotalMemoryMB, src.MaxMemoryMB, src.LastPatchDate,
                src.MaxDOP, src.CostThreshold, src.LastSeen);
END;

GO

-- [dashboard].[usp_Get_EOS_DashboardData]

-- =====================================================================
-- 6. STORED PROCEDURE FOR DASHBOARD DATA (JSON output for API)
-- =====================================================================
CREATE   PROCEDURE [dashboard].[usp_Get_EOS_DashboardData]
AS
BEGIN
    SET NOCOUNT ON;

    -- Return multiple result sets for dashboard

    -- Result Set 1: KPI Summary
    SELECT * FROM dashboard.vw_EOS_KPISummary;

    -- Result Set 2: Version Distribution
    SELECT * FROM dashboard.vw_EOS_VersionDistribution ORDER BY SortOrder, SQLVersion;

    -- Result Set 3: Server List (Top 100 by urgency)
    SELECT TOP 100 * FROM dashboard.vw_EOS_ServerList ORDER BY SortPriority, DaysUntilEOS;

    -- Result Set 4: Active Alerts
    SELECT * FROM dashboard.vw_EOS_Alerts;

    -- Result Set 5: Timeline Data
    SELECT * FROM dashboard.vw_EOS_Timeline ORDER BY EOSDate;
END

GO

-- [dashboard].[usp_Get_EOS_JSON]

-- =====================================================================
-- 7. JSON API PROCEDURE (for AJAX calls)
-- =====================================================================
CREATE   PROCEDURE [dashboard].[usp_Get_EOS_JSON]
    @DataType VARCHAR(20) = 'summary'  -- summary, servers, alerts, timeline, all
AS
BEGIN
    SET NOCOUNT ON;

    IF @DataType = 'summary' OR @DataType = 'all'
    BEGIN
        SELECT 
            TotalServers,
            EOLCount,
            CriticalCount,
            HighCount,
            MediumCount,
            SupportedCount,
            LastUpdated
        FROM dashboard.vw_EOS_KPISummary
        FOR JSON PATH, WITHOUT_ARRAY_WRAPPER;
    END

    IF @DataType = 'servers' OR @DataType = 'all'
    BEGIN
        SELECT 
            ServerName,
            InstanceName,
            SQLVersion,
            Edition,
            EndOfSupportDate,
            DaysUntilEOS,
            UrgencyLevel,
            StatusIcon,
            StatusColor,
            RecommendedAction
        FROM dashboard.vw_EOS_ServerList
        ORDER BY SortPriority, DaysUntilEOS
        FOR JSON PATH;
    END

    IF @DataType = 'alerts' OR @DataType = 'all'
    BEGIN
        SELECT 
            ServerName,
            SQLVersion,
            AlertMessage,
            AlertType,
            DaysUntilEOS
        FROM dashboard.vw_EOS_Alerts
        FOR JSON PATH;
    END

    IF @DataType = 'timeline' OR @DataType = 'all'
    BEGIN
        SELECT 
            [Version],
            [Servers],
            [EOSDateFormatted],
            [DaysRemaining],
            [TimelineStatus],
            [UrgencyPercent]
        FROM dashboard.vw_EOS_Timeline
        ORDER BY EOSDate
        FOR JSON PATH;
    END
END

GO

-- [dashboard].[usp_GetActiveAlerts]

CREATE   PROCEDURE dashboard.usp_GetActiveAlerts
    @ServerName  nvarchar(128) = NULL,
    @Severity    nvarchar(50)  = NULL,
    @AlertType   nvarchar(200) = NULL,
    @FromUtc     datetime2     = NULL,
    @ToUtc       datetime2     = NULL,
    @OnlyOpen    bit           = 1
AS
BEGIN
    SET NOCOUNT ON;

    IF @ServerName IS NOT NULL AND LTRIM(RTRIM(@ServerName)) = '' SET @ServerName = NULL;
    IF @Severity   IS NOT NULL AND LTRIM(RTRIM(@Severity))   = '' SET @Severity   = NULL;
    IF @AlertType  IS NOT NULL AND LTRIM(RTRIM(@AlertType))  = '' SET @AlertType  = NULL;

    DECLARE @AlertTypeCol sysname =
        CASE
            WHEN COL_LENGTH('alert.ActiveAlert','AlertType') IS NOT NULL THEN 'AlertType'
            WHEN COL_LENGTH('alert.ActiveAlert','AlertName') IS NOT NULL THEN 'AlertName'
            WHEN COL_LENGTH('alert.ActiveAlert','Alert')     IS NOT NULL THEN 'Alert'
            ELSE NULL
        END;

    DECLARE @DetectedCol sysname =
        CASE
            WHEN COL_LENGTH('alert.ActiveAlert','DetectedAtUtc') IS NOT NULL THEN 'DetectedAtUtc'
            WHEN COL_LENGTH('alert.ActiveAlert','DetectedAt')    IS NOT NULL THEN 'DetectedAt'
            WHEN COL_LENGTH('alert.ActiveAlert','CreatedUtc')    IS NOT NULL THEN 'CreatedUtc'
            WHEN COL_LENGTH('alert.ActiveAlert','CreatedDate')   IS NOT NULL THEN 'CreatedDate'
            ELSE NULL
        END;

    DECLARE @OpenPredicate nvarchar(max) =
        CASE
            WHEN @OnlyOpen = 0 THEN N'1=1'
            WHEN COL_LENGTH('alert.ActiveAlert','IsResolved') IS NOT NULL THEN N'a.IsResolved = 0'
            WHEN COL_LENGTH('alert.ActiveAlert','ResolvedAtUtc') IS NOT NULL THEN N'a.ResolvedAtUtc IS NULL'
            WHEN COL_LENGTH('alert.ActiveAlert','ResolvedAt') IS NOT NULL THEN N'a.ResolvedAt IS NULL'
            WHEN COL_LENGTH('alert.ActiveAlert','Status') IS NOT NULL THEN N'ISNULL(a.Status,'''') NOT IN (''Resolved'',''Closed'')'
            ELSE N'1=1'
        END;

    DECLARE @sql nvarchar(max) = N'
SELECT a.*
FROM alert.ActiveAlert a
WHERE 1=1
  AND (@ServerName IS NULL OR a.ServerName = @ServerName)
  AND (@Severity   IS NULL OR a.Severity   = @Severity)
  AND (' + @OpenPredicate + N') ';

    IF @AlertTypeCol IS NOT NULL
        SET @sql += N' AND (@AlertType IS NULL OR a.' + QUOTENAME(@AlertTypeCol) + N' = @AlertType) ';

    IF @DetectedCol IS NOT NULL
        SET @sql += N' AND (@FromUtc IS NULL OR a.' + QUOTENAME(@DetectedCol) + N' >= @FromUtc)
                      AND (@ToUtc   IS NULL OR a.' + QUOTENAME(@DetectedCol) + N' <= @ToUtc) ';

    IF @DetectedCol IS NOT NULL
        SET @sql += N' ORDER BY a.' + QUOTENAME(@DetectedCol) + N' DESC;';
    ELSE
        SET @sql += N' ORDER BY a.ServerName;';

    EXEC sys.sp_executesql
        @sql,
        N'@ServerName nvarchar(128), @Severity nvarchar(50), @AlertType nvarchar(200), @FromUtc datetime2, @ToUtc datetime2',
        @ServerName, @Severity, @AlertType, @FromUtc, @ToUtc;
END

GO

-- [dashboard].[usp_GetDashboardJSON]

-- =============================================================================
-- PROCEDURE: dashboard.usp_GetDashboardJSON
-- Generates JSON for data.json file (used by existing dashboard)
-- =============================================================================
CREATE   PROCEDURE dashboard.usp_GetDashboardJSON
AS
BEGIN
    SET NOCOUNT ON;
    
    -- Build comprehensive JSON for dashboard
    SELECT (
        SELECT 
            -- Incidents section
            (SELECT * FROM dashboard.vw_IncidentSummary FOR JSON PATH, WITHOUT_ARRAY_WRAPPER) AS incidentSummary,
            (SELECT * FROM dashboard.vw_RecentIncidents FOR JSON PATH) AS recentIncidents,
            (SELECT * FROM dashboard.vw_IncidentsByCMSGroup FOR JSON PATH) AS incidentsByGroup,
            
            -- Changes section
            (SELECT * FROM dashboard.vw_ChangeTimeline24h FOR JSON PATH) AS recentChanges,
            
            -- Compliance section
            (SELECT * FROM dashboard.vw_ComplianceSummary FOR JSON PATH) AS compliance,
            
            -- Playbooks section
            (SELECT * FROM dashboard.vw_PlaybookSummary FOR JSON PATH, WITHOUT_ARRAY_WRAPPER) AS playbooks,
            
            -- Metrics section
            (SELECT * FROM dashboard.vw_DashboardMetrics FOR JSON PATH, WITHOUT_ARRAY_WRAPPER) AS metrics
        FOR JSON PATH, WITHOUT_ARRAY_WRAPPER
    ) AS DashboardJSON;
END

GO

-- [dashboard].[usp_GetJobsMonitor]

CREATE   PROCEDURE dashboard.usp_GetJobsMonitor
    @ServerName   nvarchar(128) = NULL,
    @JobName      nvarchar(256) = NULL,
    @FromUtc      datetime2     = NULL,
    @ToUtc        datetime2     = NULL,
    @DBAOpsOnly   bit           = 0
AS
BEGIN
    SET NOCOUNT ON;

    IF @ServerName IS NOT NULL AND LTRIM(RTRIM(@ServerName)) = '' SET @ServerName = NULL;
    IF @JobName    IS NOT NULL AND LTRIM(RTRIM(@JobName))    = '' SET @JobName    = NULL;

    SELECT
        ServerName,
        JobName,
        StepName,
        EventTimeUtc,
        RunStatus,
        Message
    FROM dashboard.vw_JobsMonitor
    WHERE (@ServerName IS NULL OR ServerName = @ServerName)
      AND (@JobName IS NULL OR JobName LIKE @JobName + '%')
      AND (@FromUtc IS NULL OR EventTimeUtc >= @FromUtc)
      AND (@ToUtc   IS NULL OR EventTimeUtc <= @ToUtc)
      AND (@DBAOpsOnly = 0 OR JobName LIKE 'DBAOps%')
    ORDER BY EventTimeUtc DESC;
END

GO

-- [dashboard].[usp_SearchRemediation]

CREATE   PROCEDURE dashboard.usp_SearchRemediation
    @SearchTerm NVARCHAR(255) = NULL,
    @Category NVARCHAR(50) = NULL, -- 'Disk', 'Patch', 'Backup', 'Security', 'Performance', 'All'
    @Severity NVARCHAR(20) = NULL, -- 'Critical', 'Warning', 'Caution', 'All'
    @ServerName NVARCHAR(128) = NULL,
    @MaxResults INT = 100
AS
BEGIN
    SET NOCOUNT ON;
    
    /*
    Infrastructure-wide Remediation Search
    Searches across all monitoring categories and returns:
    - Issues found
    - Remediation steps
    - T-SQL scripts where applicable
    - Links to documentation
    
    Government Compliance: FedRAMP High, CJIS, DoD IL4/IL5/IL6
    */
    
    -- Return compliance information header
    SELECT 
        'Azure Government' AS CloudEnvironment,
        'FedRAMP High, CJIS, DoD IL4, DoD IL5, DoD IL6' AS ComplianceFrameworks,
        'This infrastructure operates under government compliance requirements' AS ComplianceNote;
    
    -- Temp table for results
    CREATE TABLE #RemediationResults (
        ResultID INT IDENTITY(1,1),
        Category NVARCHAR(50),
        Severity NVARCHAR(20),
        ServerName NVARCHAR(128),
        Issue NVARCHAR(500),
        CurrentValue NVARCHAR(100),
        Threshold NVARCHAR(100),
        RemediationSteps NVARCHAR(MAX),
        TSQLScript NVARCHAR(MAX),
        DocumentationLink NVARCHAR(500),
        DetectedAt DATETIME2
    );
    
    -- ========== DISK SPACE ISSUES ==========
    IF @Category IS NULL OR @Category IN ('Disk', 'All')
    BEGIN
        INSERT INTO #RemediationResults (Category, Severity, ServerName, Issue, CurrentValue, Threshold, RemediationSteps, TSQLScript, DocumentationLink, DetectedAt)
        SELECT 
            'Disk Space' AS Category,
            AlertLevel AS Severity,
            ServerName,
            'Low disk space on ' + DriveLetter + ': (' + VolumeName + ')' AS Issue,
            CAST(FreeSpacePercent AS VARCHAR) + '% free (' + CAST(CAST(FreeSpaceGB AS INT) AS VARCHAR) + ' GB)' AS CurrentValue,
            CASE AlertLevel
                WHEN 'Critical' THEN '< 5% free'
                WHEN 'Warning' THEN '< 25% free'
                WHEN 'Caution' THEN '< 50% free'
            END AS Threshold,
            CASE AlertLevel
                WHEN 'Critical' THEN '1. IMMEDIATE: Expand disk or move data
2. Delete old backup files
3. Shrink log files if possible
4. Contact storage team for emergency expansion'
                WHEN 'Warning' THEN '1. Schedule disk expansion
2. Review and archive old files
3. Check for unexpected growth
4. Plan capacity increase'
                ELSE '1. Monitor growth trend
2. Schedule maintenance window
3. Plan capacity review'
            END AS RemediationSteps,
            '-- Check largest files/tables on this server
USE master;
EXEC sp_MSforeachdb ''USE [?]; SELECT TOP 10 DB_NAME() AS DatabaseName, 
    name AS FileName, size/128.0 AS SizeMB, 
    FILEPROPERTY(name, ''''SpaceUsed'''')/128.0 AS UsedMB
FROM sys.database_files ORDER BY size DESC'';

-- Shrink log file (use with caution)
-- DBCC SHRINKFILE (''LogFileName'', 1024);' AS TSQLScript,
            'https://sharepoint.YOURORG.gov/IT/DBA/DiskSpace_SOP.pdf' AS DocumentationLink,
            CollectedAt AS DetectedAt
        FROM dashboard.vw_DiskSpaceMonitor
        WHERE AlertLevel IN ('Critical', 'Warning', 'Caution')
        AND (@ServerName IS NULL OR ServerName LIKE '%' + @ServerName + '%')
        AND (@Severity IS NULL OR @Severity = 'All' OR AlertLevel = @Severity);
    END
    
    -- ========== PATCHING ISSUES ==========
    IF @Category IS NULL OR @Category IN ('Patch', 'All')
    BEGIN
        INSERT INTO #RemediationResults (Category, Severity, ServerName, Issue, CurrentValue, Threshold, RemediationSteps, TSQLScript, DocumentationLink, DetectedAt)
        SELECT 
            'Patching' AS Category,
            CASE 
                WHEN MissingCriticalPatches > 0 THEN 'Critical'
                WHEN MissingPatches > 3 THEN 'Warning'
                ELSE 'Caution'
            END AS Severity,
            ServerName,
            'Server requires SQL patches' AS Issue,
            CAST(MissingPatches AS VARCHAR) + ' patches missing (' + CAST(MissingCriticalPatches AS VARCHAR) + ' critical)' AS CurrentValue,
            'Compliant = 0 missing patches' AS Threshold,
            '1. Review missing patches in Tanium
2. Schedule maintenance window
3. Download and apply CU from Microsoft
4. Verify installation and restart if needed
5. Update compliance status' AS RemediationSteps,
            '-- Check current SQL version
SELECT @@VERSION;
SELECT SERVERPROPERTY(''ProductVersion'') AS Version,
       SERVERPROPERTY(''ProductLevel'') AS ServicePack,
       SERVERPROPERTY(''ProductUpdateLevel'') AS CumulativeUpdate;

-- After patching, verify:
SELECT @@VERSION;' AS TSQLScript,
            'https://sharepoint.YOURORG.gov/IT/DBA/Patching_SOP.pdf' AS DocumentationLink,
            GETDATE() AS DetectedAt
        FROM Patching.TaniumPatchStatus
        WHERE IsActive = 1
        AND ComplianceStatus = 'Non-Compliant'
        AND (@ServerName IS NULL OR ServerName LIKE '%' + @ServerName + '%')
        AND (@Severity IS NULL OR @Severity = 'All' OR 
            (@Severity = 'Critical' AND MissingCriticalPatches > 0) OR
            (@Severity = 'Warning' AND MissingPatches > 3));
    END
    
    -- ========== BACKUP ISSUES ==========
    IF @Category IS NULL OR @Category IN ('Backup', 'All')
    BEGIN
        INSERT INTO #RemediationResults (Category, Severity, ServerName, Issue, CurrentValue, Threshold, RemediationSteps, TSQLScript, DocumentationLink, DetectedAt)
        SELECT TOP 50
            'Backup' AS Category,
            CASE 
                WHEN FullAgeHours > 48 THEN 'Critical'
                WHEN FullAgeHours > 24 THEN 'Warning'
                ELSE 'Caution'
            END AS Severity,
            ServerName,
            'Backup overdue for database: ' + DatabaseName AS Issue,
            CAST(FullAgeHours AS VARCHAR) + ' hours since last full backup' AS CurrentValue,
            'Full backup within 24 hours' AS Threshold,
            '1. Check SQL Agent job status
2. Verify backup destination accessibility
3. Check disk space on backup target
4. Run manual backup if needed
5. Investigate root cause' AS RemediationSteps,
            '-- Run immediate full backup
BACKUP DATABASE [' + DatabaseName + '] 
TO DISK = ''\\BackupShare\' + ServerName + '\' + DatabaseName + '_' + CONVERT(VARCHAR, GETDATE(), 112) + '.bak''
WITH COMPRESSION, CHECKSUM, STATS = 10;

-- Check backup history
SELECT TOP 5 database_name, backup_start_date, backup_finish_date, 
       backup_size/1024/1024 AS SizeMB, compressed_backup_size/1024/1024 AS CompressedMB
FROM msdb.dbo.backupset 
WHERE database_name = ''' + DatabaseName + '''
ORDER BY backup_start_date DESC;' AS TSQLScript,
            'https://sharepoint.YOURORG.gov/IT/DBA/Backup_SOP.pdf' AS DocumentationLink,
            LastBackupTime AS DetectedAt
        FROM ctl.BackupCompliance
        WHERE IsCompliant = 0
        AND (@ServerName IS NULL OR ServerName LIKE '%' + @ServerName + '%')
        AND (@Severity IS NULL OR @Severity = 'All' OR 
            (@Severity = 'Critical' AND FullAgeHours > 48) OR
            (@Severity = 'Warning' AND FullAgeHours > 24));
    END
    
    -- ========== PERFORMANCE ISSUES ==========
    IF @Category IS NULL OR @Category IN ('Performance', 'All')
    BEGIN
        INSERT INTO #RemediationResults (Category, Severity, ServerName, Issue, CurrentValue, Threshold, RemediationSteps, TSQLScript, DocumentationLink, DetectedAt)
        SELECT TOP 50
            'Performance' AS Category,
            CASE 
                WHEN CPU_Utilization > 90 THEN 'Critical'
                WHEN CPU_Utilization > 75 THEN 'Warning'
                ELSE 'Caution'
            END AS Severity,
            ServerName,
            'High CPU utilization detected' AS Issue,
            CAST(CPU_Utilization AS VARCHAR) + '% CPU' AS CurrentValue,
            '< 75% sustained' AS Threshold,
            '1. Identify top CPU consumers
2. Check for missing indexes
3. Review query execution plans
4. Check for blocking
5. Consider workload optimization' AS RemediationSteps,
            '-- Find top CPU consuming queries
SELECT TOP 10 
    qs.total_worker_time/1000000 AS TotalCPU_Seconds,
    qs.execution_count,
    qs.total_worker_time/qs.execution_count/1000 AS AvgCPU_ms,
    SUBSTRING(qt.text, (qs.statement_start_offset/2)+1,
        ((CASE qs.statement_end_offset WHEN -1 THEN DATALENGTH(qt.text)
        ELSE qs.statement_end_offset END - qs.statement_start_offset)/2)+1) AS QueryText
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) qt
ORDER BY qs.total_worker_time DESC;' AS TSQLScript,
            'https://sharepoint.YOURORG.gov/IT/DBA/Performance_SOP.pdf' AS DocumentationLink,
            CaptureTime AS DetectedAt
        FROM dbo.FACT_ServerHealth
        WHERE CPU_Utilization > 75
        AND (@ServerName IS NULL OR ServerName LIKE '%' + @ServerName + '%')
        AND CaptureTime > DATEADD(HOUR, -24, GETDATE());
        
        -- Memory pressure
        INSERT INTO #RemediationResults (Category, Severity, ServerName, Issue, CurrentValue, Threshold, RemediationSteps, TSQLScript, DocumentationLink, DetectedAt)
        SELECT TOP 50
            'Performance' AS Category,
            CASE 
                WHEN Memory_PLE < 100 THEN 'Critical'
                WHEN Memory_PLE < 300 THEN 'Warning'
                ELSE 'Caution'
            END AS Severity,
            ServerName,
            'Low Page Life Expectancy (Memory Pressure)' AS Issue,
            CAST(Memory_PLE AS VARCHAR) + ' seconds PLE' AS CurrentValue,
            '> 300 seconds' AS Threshold,
            '1. Check memory grants pending
2. Review memory-intensive queries
3. Verify Max Server Memory setting
4. Consider memory upgrade
5. Review index usage' AS RemediationSteps,
            '-- Check memory configuration
SELECT 
    physical_memory_kb/1024 AS Physical_MB,
    committed_kb/1024 AS Committed_MB,
    committed_target_kb/1024 AS Target_MB
FROM sys.dm_os_sys_info;

-- Check memory clerks
SELECT TOP 10 type, SUM(pages_kb)/1024 AS Size_MB
FROM sys.dm_os_memory_clerks
GROUP BY type ORDER BY Size_MB DESC;' AS TSQLScript,
            'https://sharepoint.YOURORG.gov/IT/DBA/Memory_SOP.pdf' AS DocumentationLink,
            CaptureTime AS DetectedAt
        FROM dbo.FACT_ServerHealth
        WHERE Memory_PLE < 300 AND Memory_PLE IS NOT NULL
        AND (@ServerName IS NULL OR ServerName LIKE '%' + @ServerName + '%')
        AND CaptureTime > DATEADD(HOUR, -24, GETDATE());
    END
    
    -- Return results
    SELECT TOP (@MaxResults)
        Category,
        Severity,
        ServerName,
        Issue,
        CurrentValue,
        Threshold,
        RemediationSteps,
        TSQLScript,
        DocumentationLink,
        DetectedAt,
        -- Add compliance badge
        'FedRAMP High | CJIS | DoD IL4/IL5' AS ComplianceFrameworks
    FROM #RemediationResults
    WHERE (@SearchTerm IS NULL OR 
           Issue LIKE '%' + @SearchTerm + '%' OR 
           ServerName LIKE '%' + @SearchTerm + '%' OR
           RemediationSteps LIKE '%' + @SearchTerm + '%')
    ORDER BY 
        CASE Severity 
            WHEN 'Critical' THEN 1 
            WHEN 'Warning' THEN 2 
            WHEN 'Caution' THEN 3 
            ELSE 4 
        END,
        Category,
        ServerName;
    
    -- Return summary
    SELECT 
        Category,
        COUNT(*) AS IssueCount,
        SUM(CASE WHEN Severity = 'Critical' THEN 1 ELSE 0 END) AS CriticalCount,
        SUM(CASE WHEN Severity = 'Warning' THEN 1 ELSE 0 END) AS WarningCount,
        SUM(CASE WHEN Severity = 'Caution' THEN 1 ELSE 0 END) AS CautionCount
    FROM #RemediationResults
    GROUP BY Category
    ORDER BY 
        SUM(CASE WHEN Severity = 'Critical' THEN 1 ELSE 0 END) DESC,
        COUNT(*) DESC;
    
    DROP TABLE #RemediationResults;
END;

GO

-- [DBAOps].[usp_AggregateServerPerformanceDaily]
CREATE OR ALTER PROCEDURE DBAOps.usp_AggregateServerPerformanceDaily
AS
BEGIN
    SET NOCOUNT ON;

    ;WITH DailyAgg AS
    (
        SELECT
            ServerName,
            CAST(CaptureTime AS date) AS CaptureDate,
            AVG(CPUPercent)        AS AvgCPUPercent,
            MAX(CPUPercent)        AS MaxCPUPercent,
            AVG(MemoryPercent)     AS AvgMemoryPercent,
            MAX(MemoryPercent)     AS MaxMemoryPercent,
            AVG(DiskLatencyMs)     AS AvgDiskLatencyMs,
            MAX(DiskLatencyMs)     AS MaxDiskLatencyMs
        FROM DBAOps.PerformanceTelemetry
        WHERE ServerName IS NOT NULL
        GROUP BY
            ServerName,
            CAST(CaptureTime AS date)
    )
    MERGE DBAOps.ServerPerformanceDaily AS tgt
    USING DailyAgg AS src
        ON  tgt.ServerName  = src.ServerName
        AND tgt.CaptureDate = src.CaptureDate
    WHEN MATCHED THEN
        UPDATE SET
            AvgCPUPercent      = src.AvgCPUPercent,
            MaxCPUPercent      = src.MaxCPUPercent,
            AvgMemoryPercent   = src.AvgMemoryPercent,
            MaxMemoryPercent   = src.MaxMemoryPercent,
            AvgDiskLatencyMs   = src.AvgDiskLatencyMs,
            MaxDiskLatencyMs   = src.MaxDiskLatencyMs,
            LoadDate           = SYSUTCDATETIME()
    WHEN NOT MATCHED THEN
        INSERT
        (
            ServerName,
            CaptureDate,
            AvgCPUPercent,
            MaxCPUPercent,
            AvgMemoryPercent,
            MaxMemoryPercent,
            AvgDiskLatencyMs,
            MaxDiskLatencyMs,
            LoadDate
        )
        VALUES
        (
            src.ServerName,
            src.CaptureDate,
            src.AvgCPUPercent,
            src.MaxCPUPercent,
            src.AvgMemoryPercent,
            src.MaxMemoryPercent,
            src.AvgDiskLatencyMs,
            src.MaxDiskLatencyMs,
            SYSUTCDATETIME()
        );
END;

GO

-- [DBAOps].[usp_CleanupHistory]

CREATE PROCEDURE DBAOps.usp_CleanupHistory
    @RetentionDays INT = 90
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @TotalDeleted INT = 0;
    DECLARE @Deleted INT = 0;
    DECLARE @CutoffDate DATETIME2 = DATEADD(DAY, -@RetentionDays, SYSDATETIME());
    
    -- Clean old performance telemetry (CaptureTime column)
    DELETE FROM DBAOps.PerformanceTelemetry WHERE CaptureTime < @CutoffDate;
    SET @Deleted = @@ROWCOUNT; SET @TotalDeleted = @TotalDeleted + @Deleted;
    PRINT '  Deleted ' + CAST(@Deleted AS VARCHAR(10)) + ' from PerformanceTelemetry';
    
    -- Clean old wait stats (CollectionTime column)
    DELETE FROM DBAOps.WaitStatsHistory WHERE CollectionTime < @CutoffDate;
    SET @Deleted = @@ROWCOUNT; SET @TotalDeleted = @TotalDeleted + @Deleted;
    PRINT '  Deleted ' + CAST(@Deleted AS VARCHAR(10)) + ' from WaitStatsHistory';
    
    -- Clean old disk space history (CollectionTime column)
    DELETE FROM DBAOps.DiskSpaceHistory WHERE CollectionTime < @CutoffDate;
    SET @Deleted = @@ROWCOUNT; SET @TotalDeleted = @TotalDeleted + @Deleted;
    PRINT '  Deleted ' + CAST(@Deleted AS VARCHAR(10)) + ' from DiskSpaceHistory';
    
    -- Clean old automation logs (LogTime column)
    DELETE FROM log.AutomationLog WHERE LogTime < @CutoffDate;
    SET @Deleted = @@ROWCOUNT; SET @TotalDeleted = @TotalDeleted + @Deleted;
    PRINT '  Deleted ' + CAST(@Deleted AS VARCHAR(10)) + ' from AutomationLog';
    
    -- Clean old backup compliance (keep 365 days - LoadDate column)
    DELETE FROM fact.BackupCompliance WHERE LoadDate < DATEADD(DAY, -365, SYSDATETIME());
    SET @Deleted = @@ROWCOUNT; SET @TotalDeleted = @TotalDeleted + @Deleted;
    PRINT '  Deleted ' + CAST(@Deleted AS VARCHAR(10)) + ' from BackupCompliance';
    
    -- Log the operation
    INSERT INTO log.AutomationLog (ModuleName, RunStatus, Message, ServerName, RowsAffected, LogType, LogTime)
    VALUES ('CleanupHistory', 'Success', 
            'Cleaned ' + CAST(@TotalDeleted AS VARCHAR(10)) + ' rows older than ' + CAST(@RetentionDays AS VARCHAR(10)) + ' days',
            @@SERVERNAME, @TotalDeleted, 'INFO', SYSDATETIME());
    
    PRINT 'History cleanup completed: ' + CAST(@TotalDeleted AS VARCHAR(10)) + ' total rows deleted';
    
    SELECT @TotalDeleted AS TotalRowsDeleted, @RetentionDays AS RetentionDays;
END;

GO

-- [DBAOps].[usp_CollectBackupTelemetry]

-- ============================================================================
-- PROCEDURE: Collect Backup Telemetry from Target Server
-- ============================================================================
CREATE   PROCEDURE DBAOps.usp_CollectBackupTelemetry
    @TargetServer NVARCHAR(128)
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @SQL NVARCHAR(MAX);
    
    SET @SQL = N'
    SELECT 
        @@SERVERNAME AS ServerName,
        database_name AS DatabaseName,
        CASE type
            WHEN ''D'' THEN ''Full''
            WHEN ''I'' THEN ''Differential''
            WHEN ''L'' THEN ''Transaction Log''
        END AS BackupType,
        backup_start_date AS BackupStartDate,
        backup_finish_date AS BackupFinishDate,
        DATEDIFF(SECOND, backup_start_date, backup_finish_date) AS BackupDurationSeconds,
        CAST(backup_size / 1024.0 / 1024.0 / 1024.0 AS DECIMAL(18,2)) AS BackupSizeGB,
        CAST(compressed_backup_size / 1024.0 / 1024.0 / 1024.0 AS DECIMAL(18,2)) AS CompressedSizeGB,
        CAST(100.0 * compressed_backup_size / NULLIF(backup_size, 0) AS DECIMAL(5,2)) AS CompressionRatio,
        physical_device_name AS BackupPath,
        CASE WHEN backup_finish_date IS NOT NULL THEN 1 ELSE 0 END AS IsSuccessful,
        NULL AS ErrorMessage,
        SYSDATETIME() AS CaptureDate
    FROM msdb.dbo.backupset
    WHERE backup_start_date >= DATEADD(HOUR, -24, GETDATE())
    ORDER BY backup_start_date DESC';
    
    -- Insert backup telemetry using linked server or dynamic SQL
    INSERT INTO DBAOps.BackupTelemetry (ServerName, DatabaseName, BackupType, 
                                        BackupStartDate, BackupFinishDate, 
                                        BackupDurationSeconds, BackupSizeGB, 
                                        CompressedSizeGB, CompressionRatio, 
                                        BackupPath, IsSuccessful, ErrorMessage, CaptureDate)
    EXEC(@SQL) AT [YourLinkedServerName]; -- Replace with your linked server name
    
    PRINT 'Backup telemetry collected for ' + @TargetServer;
END;

GO

-- [DBAOps].[usp_CollectDatabaseInventory]

    CREATE PROCEDURE DBAOps.usp_CollectDatabaseInventory
        @ServerName NVARCHAR(128) = NULL
    AS
    BEGIN
        SET NOCOUNT ON;
        
        IF @ServerName IS NULL
            SET @ServerName = @@SERVERNAME;
        
        -- Collect database inventory for local server
        INSERT INTO DBAOps.DatabaseInventory (
            ServerName, DatabaseName, DatabaseID, OwnerName, CreateDate,
            CompatibilityLevel, RecoveryModel, StateDesc, Collation,
            IsReadOnly, IsAutoShrink, IsAutoClose, PageVerifyOption
        )
        SELECT 
            @ServerName,
            d.name,
            d.database_id,
            SUSER_SNAME(d.owner_sid),
            d.create_date,
            d.compatibility_level,
            d.recovery_model_desc,
            d.state_desc,
            d.collation_name,
            d.is_read_only,
            d.is_auto_shrink_on,
            d.is_auto_close_on,
            d.page_verify_option_desc
        FROM sys.databases d
        WHERE d.database_id > 4  -- Exclude system databases
          AND NOT EXISTS (
              SELECT 1 FROM DBAOps.DatabaseInventory di 
              WHERE di.ServerName = @ServerName 
                AND di.DatabaseName = d.name
                AND di.CollectionTime > DATEADD(HOUR, -1, SYSDATETIME())
          );
        
        PRINT 'Database inventory collected: ' + CAST(@@ROWCOUNT AS VARCHAR(10)) + ' databases';
    END
    
GO

-- [DBAOps].[usp_CollectOlaDeploymentStatus]

CREATE PROCEDURE DBAOps.usp_CollectOlaDeploymentStatus
AS
BEGIN
    /*
        Collects Ola Hallengren deployment status from all CMS servers
        Run this periodically to populate the tracking table
    */
    SET NOCOUNT ON;
    
    DECLARE @ServerName NVARCHAR(255);
    DECLARE @SQL NVARCHAR(MAX);
    
    -- Cursor through CMS servers
    DECLARE ServerCursor CURSOR LOCAL FAST_FORWARD FOR
        SELECT DISTINCT server_name 
        FROM msdb.dbo.sysmanagement_shared_registered_servers_internal
        ORDER BY server_name;
    
    OPEN ServerCursor;
    FETCH NEXT FROM ServerCursor INTO @ServerName;
    
    WHILE @@FETCH_STATUS = 0
    BEGIN
        BEGIN TRY
            -- Check for Ola objects on remote server
            SET @SQL = N'
            DECLARE @HasCommandLog BIT = 0;
            DECLARE @HasCommandExecute BIT = 0;
            DECLARE @HasDatabaseBackup BIT = 0;
            DECLARE @HasIntegrityCheck BIT = 0;
            DECLARE @HasIndexOptimize BIT = 0;
            
            -- Check for CommandLog table
            IF EXISTS (SELECT 1 FROM [' + @ServerName + '].master.sys.objects WHERE name = ''CommandLog'')
                SET @HasCommandLog = 1;
            
            -- Check for stored procedures
            IF EXISTS (SELECT 1 FROM [' + @ServerName + '].master.sys.procedures WHERE name = ''CommandExecute'')
                SET @HasCommandExecute = 1;
            IF EXISTS (SELECT 1 FROM [' + @ServerName + '].master.sys.procedures WHERE name = ''DatabaseBackup'')
                SET @HasDatabaseBackup = 1;
            IF EXISTS (SELECT 1 FROM [' + @ServerName + '].master.sys.procedures WHERE name = ''DatabaseIntegrityCheck'')
                SET @HasIntegrityCheck = 1;
            IF EXISTS (SELECT 1 FROM [' + @ServerName + '].master.sys.procedures WHERE name = ''IndexOptimize'')
                SET @HasIndexOptimize = 1;
            
            -- Upsert into tracking table
            MERGE DBAOpsRepository.DBAOps.OlaDeploymentTracking AS Target
            USING (SELECT ''' + @ServerName + ''' AS ServerName) AS Source
            ON Target.ServerName = Source.ServerName
            WHEN MATCHED THEN
                UPDATE SET 
                    CommandLogExists = @HasCommandLog,
                    CommandExecuteExists = @HasCommandExecute,
                    DatabaseBackupExists = @HasDatabaseBackup,
                    DatabaseIntegrityCheckExists = @HasIntegrityCheck,
                    IndexOptimizeExists = @HasIndexOptimize,
                    LastChecked = GETDATE()
            WHEN NOT MATCHED THEN
                INSERT (ServerName, CommandLogExists, CommandExecuteExists, DatabaseBackupExists, 
                        DatabaseIntegrityCheckExists, IndexOptimizeExists, LastChecked)
                VALUES (''' + @ServerName + ''', @HasCommandLog, @HasCommandExecute, @HasDatabaseBackup,
                        @HasIntegrityCheck, @HasIndexOptimize, GETDATE());
            ';
            
            EXEC sp_executesql @SQL;
            
        END TRY
        BEGIN CATCH
            PRINT 'Error checking ' + @ServerName + ': ' + ERROR_MESSAGE();
        END CATCH
        
        FETCH NEXT FROM ServerCursor INTO @ServerName;
    END
    
    CLOSE ServerCursor;
    DEALLOCATE ServerCursor;
    
    PRINT 'Ola deployment status collection completed.';
END

GO

-- [DBAOps].[usp_CollectPerformanceTelemetry]
CREATE OR ALTER PROCEDURE DBAOps.usp_CollectPerformanceTelemetry
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @ServerName sysname =
        CAST(SERVERPROPERTY('ServerName') AS sysname);

    INSERT INTO DBAOps.PerformanceTelemetry
    (
        ServerName,
        DatabaseName,
        DatabaseID,
        QueryHash,
        ExecutionCount,
        TotalCPU,
        TotalDuration,
        TotalReads,
        CaptureTime
    )
    SELECT
        @ServerName,
        ISNULL(DB_NAME(st.dbid), 'UNKNOWN'),
        st.dbid,
        qs.query_hash,
        qs.execution_count,
        qs.total_worker_time,
        qs.total_elapsed_time,
        qs.total_logical_reads,
        GETDATE()
    FROM sys.dm_exec_query_stats qs
    CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st;
END;

GO

-- [DBAOps].[usp_CollectServerHealth]
CREATE OR ALTER PROCEDURE DBAOps.usp_CollectServerHealth
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @CPUPercent decimal(5,2);

    /* CPU % from ring buffers (authoritative method) */
    SELECT TOP (1)
        @CPUPercent =
        100.0 - CAST(rb_xml.value(
            '(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]',
            'int'
        ) AS decimal(5,2))
    FROM sys.dm_os_ring_buffers rb
    CROSS APPLY (SELECT CAST(rb.record AS xml)) AS rbx(rb_xml)
    WHERE rb.ring_buffer_type = N'RING_BUFFER_SCHEDULER_MONITOR'
      AND rb.record LIKE '%<SystemHealth>%'
    ORDER BY rb.timestamp DESC;

    /* Insert server health snapshot */
    INSERT INTO DBAOps.ServerHealthTelemetry
    (
        ServerName,
        CPUPercent,
        MemoryPercent,
        DiskLatencyMs
    )
    SELECT
        @@SERVERNAME,
        @CPUPercent,
        /* Memory % */
        100.0 * (1.0 - (available_physical_memory_kb * 1.0 / total_physical_memory_kb)),
        /* Disk latency */
        io.avg_disk_latency_ms
    FROM sys.dm_os_sys_memory
    CROSS APPLY
    (
        SELECT
            AVG(
                (io_stall_read_ms + io_stall_write_ms) * 1.0 /
                NULLIF(num_of_reads + num_of_writes, 0)
            )
        FROM sys.dm_io_virtual_file_stats(NULL, NULL)
    ) io(avg_disk_latency_ms);
END;

GO

-- [DBAOps].[usp_CollectServerInventory]

    CREATE PROCEDURE DBAOps.usp_CollectServerInventory
        @ServerName NVARCHAR(128) = NULL
    AS
    BEGIN
        SET NOCOUNT ON;
        
        -- This procedure collects server inventory
        -- For linked server collection, use PowerShell or SSIS
        
        DECLARE @SQL NVARCHAR(MAX);
        DECLARE @CurrentServer NVARCHAR(128);
        
        -- If no server specified, collect for local server
        IF @ServerName IS NULL
            SET @ServerName = @@SERVERNAME;
        
        -- Update or insert server inventory
        MERGE DBAOps.ServerInventory AS target
        USING (
            SELECT 
                @ServerName AS ServerName,
                @@SERVERNAME AS InstanceName,
                SERVERPROPERTY('ProductVersion') AS SQLVersion,
                SERVERPROPERTY('Edition') AS SQLEdition,
                SERVERPROPERTY('ProductLevel') AS ProductLevel,
                SERVERPROPERTY('IsClustered') AS IsClustered,
                SERVERPROPERTY('IsHadrEnabled') AS IsAlwaysOn
        ) AS source
        ON target.ServerName = source.ServerName
        WHEN MATCHED THEN
            UPDATE SET 
                SQLVersion = CAST(source.SQLVersion AS NVARCHAR(500)),
                SQLEdition = CAST(source.SQLEdition AS NVARCHAR(256)),
                ProductLevel = CAST(source.ProductLevel AS NVARCHAR(50)),
                IsClustered = CAST(source.IsClustered AS BIT),
                IsAlwaysOn = CAST(source.IsAlwaysOn AS BIT),
                LastUpdated = SYSDATETIME()
        WHEN NOT MATCHED THEN
            INSERT (ServerName, InstanceName, SQLVersion, SQLEdition, ProductLevel, IsClustered, IsAlwaysOn)
            VALUES (source.ServerName, CAST(source.InstanceName AS NVARCHAR(256)), 
                    CAST(source.SQLVersion AS NVARCHAR(500)), CAST(source.SQLEdition AS NVARCHAR(256)),
                    CAST(source.ProductLevel AS NVARCHAR(50)), CAST(source.IsClustered AS BIT), 
                    CAST(source.IsAlwaysOn AS BIT));
        
        PRINT 'Server inventory collected for: ' + @ServerName;
    END
    
GO

-- [DBAOps].[usp_CollectWaitStats]

    CREATE PROCEDURE DBAOps.usp_CollectWaitStats
        @ServerName NVARCHAR(128) = NULL
    AS
    BEGIN
        SET NOCOUNT ON;
        
        IF @ServerName IS NULL
            SET @ServerName = @@SERVERNAME;
        
        -- Collect top wait stats
        INSERT INTO DBAOps.WaitStatsHistory (
            ServerName, WaitType, WaitingTasksCount, WaitTimeMs, 
            MaxWaitTimeMs, SignalWaitTimeMs, WaitCategory
        )
        SELECT TOP 50
            @ServerName,
            wait_type,
            waiting_tasks_count,
            wait_time_ms,
            max_wait_time_ms,
            signal_wait_time_ms,
            CASE 
                WHEN wait_type LIKE 'LCK%' THEN 'Lock'
                WHEN wait_type LIKE 'PAGEIO%' OR wait_type LIKE 'WRITELOG' THEN 'I/O'
                WHEN wait_type LIKE 'ASYNC_NETWORK%' OR wait_type LIKE 'NET%' THEN 'Network'
                WHEN wait_type LIKE 'CXPACKET%' OR wait_type LIKE 'CXCONSUMER%' THEN 'Parallelism'
                WHEN wait_type LIKE 'LATCH%' THEN 'Latch'
                WHEN wait_type LIKE 'SOS_SCHEDULER%' THEN 'CPU'
                ELSE 'Other'
            END
        FROM sys.dm_os_wait_stats
        WHERE wait_type NOT LIKE '%SLEEP%'
          AND wait_type NOT LIKE '%IDLE%'
          AND wait_type NOT LIKE '%QUEUE%'
          AND wait_type NOT IN ('CLR_AUTO_EVENT', 'REQUEST_FOR_DEADLOCK_SEARCH', 
                               'SQLTRACE_BUFFER_FLUSH', 'BROKER_TASK_STOP')
        ORDER BY wait_time_ms DESC;
        
        PRINT 'Wait stats collected: ' + CAST(@@ROWCOUNT AS VARCHAR(10)) + ' wait types';
    END
    
GO

-- [DBAOps].[usp_ComplianceAudit]

    CREATE PROCEDURE DBAOps.usp_ComplianceAudit
        @ServerName NVARCHAR(128) = NULL
    AS
    BEGIN
        SET NOCOUNT ON;
        
        IF @ServerName IS NULL
            SET @ServerName = @@SERVERNAME;
        
        -- Check various compliance items
        
        -- 1. Check page verify option
        INSERT INTO DBAOps.ComplianceChecks (ServerName, CheckCategory, CheckName, CheckDescription, ExpectedValue, ActualValue, IsCompliant, Severity)
        SELECT 
            @ServerName,
            'Database Settings',
            'Page Verify - ' + name,
            'Page verify should be CHECKSUM for data integrity',
            'CHECKSUM',
            page_verify_option_desc,
            CASE WHEN page_verify_option_desc = 'CHECKSUM' THEN 1 ELSE 0 END,
            CASE WHEN page_verify_option_desc = 'CHECKSUM' THEN 'OK' ELSE 'HIGH' END
        FROM sys.databases
        WHERE database_id > 4;
        
        -- 2. Check auto-shrink
        INSERT INTO DBAOps.ComplianceChecks (ServerName, CheckCategory, CheckName, CheckDescription, ExpectedValue, ActualValue, IsCompliant, Severity)
        SELECT 
            @ServerName,
            'Database Settings',
            'Auto Shrink - ' + name,
            'Auto shrink should be disabled',
            'OFF',
            CASE WHEN is_auto_shrink_on = 1 THEN 'ON' ELSE 'OFF' END,
            CASE WHEN is_auto_shrink_on = 0 THEN 1 ELSE 0 END,
            CASE WHEN is_auto_shrink_on = 0 THEN 'OK' ELSE 'HIGH' END
        FROM sys.databases
        WHERE database_id > 4;
        
        -- 3. Check recovery model for user databases
        INSERT INTO DBAOps.ComplianceChecks (ServerName, CheckCategory, CheckName, CheckDescription, ExpectedValue, ActualValue, IsCompliant, Severity)
        SELECT 
            @ServerName,
            'Backup Compliance',
            'Recovery Model - ' + name,
            'Production databases should use FULL recovery model',
            'FULL',
            recovery_model_desc,
            CASE WHEN recovery_model_desc = 'FULL' THEN 1 ELSE 0 END,
            CASE WHEN recovery_model_desc = 'FULL' THEN 'OK' ELSE 'MEDIUM' END
        FROM sys.databases
        WHERE database_id > 4
          AND name NOT LIKE '%Test%'
          AND name NOT LIKE '%Dev%';
        
        PRINT 'Compliance audit completed: ' + CAST(@@ROWCOUNT AS VARCHAR(10)) + ' checks';
    END
    
GO

-- [DBAOps].[usp_DispatchPerformanceTelemetry]

CREATE   PROCEDURE DBAOps.usp_DispatchPerformanceTelemetry
    @ServerName NVARCHAR(128) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    SELECT 
        cms.ServerName,
        COALESCE(cms.Environment, cms.CMSEnvironment, 'Unknown') AS Environment,
        cms.SQLVersion,
        cms.CPUCount,
        cms.TotalMemoryGB,
        cms.ComplianceTier
    FROM config.ServerInventory cms
    WHERE cms.IsActive = 1
    AND (cms.ServerType = 'SQL Server' OR cms.ServerType IS NULL)
    AND (@ServerName IS NULL OR cms.ServerName = @ServerName)
    ORDER BY cms.Environment, cms.ServerName;
END

GO

-- [DBAOps].[usp_EnforceMaintenanceWindow]
CREATE OR ALTER PROCEDURE DBAOps.usp_EnforceMaintenanceWindow
    @MaintenanceType VARCHAR(20) = 'SQL'
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @NowTime TIME = CONVERT(TIME, GETDATE());
    DECLARE @Now     DATETIME2 = SYSDATETIME();
    DECLARE @Env     VARCHAR(10);

    -- SERVER-safe environment resolution
    SET @Env =
        CASE
            WHEN @@SERVERNAME LIKE '%T'
              OR @@SERVERNAME LIKE '%DEV%'
              OR @@SERVERNAME LIKE '%TEST%'
            THEN 'NONPROD'
            ELSE 'PROD'
        END;

    -- 1ï¸âƒ£ Emergency override check
    IF EXISTS
    (
        SELECT 1
        FROM DBAOps.MaintenanceOverride
        WHERE MaintenanceType = @MaintenanceType
          AND Environment     = @Env
          AND @Now <= ValidUntil
    )
        RETURN;

    -- 2ï¸âƒ£ Normal governance window check
    IF NOT EXISTS
    (
        SELECT 1
        FROM DBAOps.MaintenanceGovernance
        WHERE MaintenanceType = @MaintenanceType
          AND Environment     = @Env
          AND Enforced        = 1
          AND @NowTime BETWEEN StartTime AND EndTime
    )
    BEGIN
        THROW 51000,
              'Outside approved SQL maintenance window (DBAOps Governance)',
              1;
    END
END;

GO

-- [DBAOps].[usp_GenerateHealthReport]

    CREATE PROCEDURE DBAOps.usp_GenerateHealthReport
        @SendEmail BIT = 0
    AS
    BEGIN
        SET NOCOUNT ON;
        
        DECLARE @ServerCount INT;
        DECLARE @DatabaseCount INT;
        DECLARE @AlertCount INT;
        DECLARE @FailedJobs INT;
        
        -- Get counts
        SELECT @ServerCount = COUNT(DISTINCT ServerName) FROM DBAOps.ServerInventory WHERE IsActive = 1;
        SELECT @DatabaseCount = COUNT(*) FROM DBAOps.DatabaseInventory WHERE CollectionTime > DATEADD(DAY, -1, GETDATE());
        SELECT @AlertCount = COUNT(*) FROM DBAOps.AlertHistory WHERE AlertTime > DATEADD(DAY, -1, GETDATE()) AND IsAcknowledged = 0;
        SELECT @FailedJobs = COUNT(*) FROM DBAOps.JobExecutionHistory WHERE RunStatus = 0 AND RunDate > DATEADD(DAY, -1, GETDATE());
        
        -- Return summary
        SELECT 
            'Health Report' AS ReportType,
            GETDATE() AS GeneratedAt,
            @ServerCount AS ActiveServers,
            @DatabaseCount AS DatabasesMonitored,
            @AlertCount AS UnacknowledgedAlerts,
            @FailedJobs AS FailedJobsLast24Hours;
        
        -- Return recent alerts
        SELECT TOP 20 
            ServerName, AlertName, Severity, AlertMessage, AlertTime
        FROM DBAOps.AlertHistory
        WHERE AlertTime > DATEADD(DAY, -1, GETDATE())
        ORDER BY AlertTime DESC;
        
        -- Return failed jobs
        SELECT TOP 20
            ServerName, JobName, RunStatusDesc, RunDate, Message
        FROM DBAOps.JobExecutionHistory
        WHERE RunStatus = 0 
          AND RunDate > DATEADD(DAY, -1, GETDATE())
        ORDER BY RunDate DESC;
    END
    
GO

-- [DBAOps].[usp_MonitorJobExecution]

    CREATE PROCEDURE DBAOps.usp_MonitorJobExecution
        @ServerName NVARCHAR(128) = NULL,
        @HoursBack INT = 24
    AS
    BEGIN
        SET NOCOUNT ON;
        
        IF @ServerName IS NULL
            SET @ServerName = @@SERVERNAME;
        
        -- Collect job execution history
        INSERT INTO DBAOps.JobExecutionHistory (
            ServerName, JobName, JobID, StepID, StepName,
            RunStatus, RunStatusDesc, RunDate, RunDurationSeconds, Message
        )
        SELECT 
            @ServerName,
            j.name,
            j.job_id,
            h.step_id,
            h.step_name,
            h.run_status,
            CASE h.run_status 
                WHEN 0 THEN 'Failed'
                WHEN 1 THEN 'Succeeded'
                WHEN 2 THEN 'Retry'
                WHEN 3 THEN 'Canceled'
                WHEN 4 THEN 'In Progress'
                ELSE 'Unknown'
            END,
            msdb.dbo.agent_datetime(h.run_date, h.run_time),
            h.run_duration / 10000 * 3600 + (h.run_duration / 100 % 100) * 60 + h.run_duration % 100,
            h.message
        FROM msdb.dbo.sysjobhistory h
        INNER JOIN msdb.dbo.sysjobs j ON h.job_id = j.job_id
        WHERE h.step_id = 0  -- Job outcome
          AND msdb.dbo.agent_datetime(h.run_date, h.run_time) > DATEADD(HOUR, -@HoursBack, GETDATE())
          AND NOT EXISTS (
              SELECT 1 FROM DBAOps.JobExecutionHistory jeh
              WHERE jeh.ServerName = @ServerName
                AND jeh.JobID = j.job_id
                AND jeh.RunDate = msdb.dbo.agent_datetime(h.run_date, h.run_time)
          );
        
        PRINT 'Job executions collected: ' + CAST(@@ROWCOUNT AS VARCHAR(10)) + ' records';
    END
    
GO

-- [DBAOps].[usp_RetentionCleanup]

    CREATE PROCEDURE DBAOps.usp_RetentionCleanup
        @RetentionDays INT = 90
    AS
    BEGIN
        SET NOCOUNT ON;
        
        DECLARE @CutoffDate DATETIME2 = DATEADD(DAY, -@RetentionDays, SYSDATETIME());
        DECLARE @TotalDeleted INT = 0;
        DECLARE @Deleted INT;
        
        -- Cleanup DBAOps tables
        DELETE FROM DBAOps.WaitStatsHistory WHERE CollectionTime < @CutoffDate;
        SET @Deleted = @@ROWCOUNT;
        SET @TotalDeleted = @TotalDeleted + @Deleted;
        IF @Deleted > 0 PRINT 'Deleted ' + CAST(@Deleted AS VARCHAR(10)) + ' rows from DBAOps.WaitStatsHistory';
        
        DELETE FROM DBAOps.JobExecutionHistory WHERE CollectionTime < @CutoffDate;
        SET @Deleted = @@ROWCOUNT;
        SET @TotalDeleted = @TotalDeleted + @Deleted;
        IF @Deleted > 0 PRINT 'Deleted ' + CAST(@Deleted AS VARCHAR(10)) + ' rows from DBAOps.JobExecutionHistory';
        
        DELETE FROM DBAOps.DiskSpaceHistory WHERE CollectionTime < @CutoffDate;
        SET @Deleted = @@ROWCOUNT;
        SET @TotalDeleted = @TotalDeleted + @Deleted;
        IF @Deleted > 0 PRINT 'Deleted ' + CAST(@Deleted AS VARCHAR(10)) + ' rows from DBAOps.DiskSpaceHistory';
        
        DELETE FROM DBAOps.ComplianceChecks WHERE CheckTime < @CutoffDate;
        SET @Deleted = @@ROWCOUNT;
        SET @TotalDeleted = @TotalDeleted + @Deleted;
        IF @Deleted > 0 PRINT 'Deleted ' + CAST(@Deleted AS VARCHAR(10)) + ' rows from DBAOps.ComplianceChecks';
        
        DELETE FROM DBAOps.ConfigurationBaseline WHERE CollectionTime < @CutoffDate;
        SET @Deleted = @@ROWCOUNT;
        SET @TotalDeleted = @TotalDeleted + @Deleted;
        IF @Deleted > 0 PRINT 'Deleted ' + CAST(@Deleted AS VARCHAR(10)) + ' rows from DBAOps.ConfigurationBaseline';
        
        DELETE FROM DBAOps.AlertHistory WHERE AlertTime < @CutoffDate AND IsAcknowledged = 1;
        SET @Deleted = @@ROWCOUNT;
        SET @TotalDeleted = @TotalDeleted + @Deleted;
        IF @Deleted > 0 PRINT 'Deleted ' + CAST(@Deleted AS VARCHAR(10)) + ' rows from DBAOps.AlertHistory';
        
        PRINT 'Total rows deleted: ' + CAST(@TotalDeleted AS VARCHAR(10));
    END
    
GO

-- [DBAOps].[usp_SelfHeal]

CREATE PROCEDURE DBAOps.usp_SelfHeal
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @ActionsPerformed INT = 0;
    DECLARE @Message NVARCHAR(MAX) = '';
    DECLARE @FailedJobCount INT = 0;
    
    -- 1. Identify failed jobs in last 24 hours
    DECLARE @FailedJobs TABLE (JobName NVARCHAR(256), FailCount INT);
    
    INSERT INTO @FailedJobs
    SELECT j.name, COUNT(*) AS FailCount
    FROM msdb.dbo.sysjobhistory h
    INNER JOIN msdb.dbo.sysjobs j ON h.job_id = j.job_id
    WHERE h.run_status = 0
      AND h.step_id = 0
      AND msdb.dbo.agent_datetime(h.run_date, h.run_time) > DATEADD(HOUR, -24, GETDATE())
      AND j.name LIKE 'DBAOps%'
      AND j.enabled = 1
    GROUP BY j.name
    HAVING COUNT(*) >= 3;
    
    SELECT @FailedJobCount = COUNT(*) FROM @FailedJobs;
    SET @ActionsPerformed = @ActionsPerformed + 1;
    
    -- 2. Clean old automation logs (keep 30 days)
    DELETE FROM log.AutomationLog WHERE LogTime < DATEADD(DAY, -30, SYSDATETIME());
    SET @ActionsPerformed = @ActionsPerformed + @@ROWCOUNT;
    
    -- 3. Update statistics on key tables
    EXEC sp_updatestats;
    SET @ActionsPerformed = @ActionsPerformed + 1;
    
    SET @Message = 'Self-heal completed. Failed jobs needing attention: ' + CAST(@FailedJobCount AS VARCHAR(10));
    
    -- Log the operation
    INSERT INTO log.AutomationLog (ModuleName, RunStatus, Message, ServerName, RowsAffected, LogType, LogTime)
    VALUES ('SelfHeal', 'Success', @Message, @@SERVERNAME, @ActionsPerformed, 'INFO', SYSDATETIME());
    
    PRINT @Message;
    
    -- Return results
    SELECT 'Self-Heal Summary' AS [Action], @ActionsPerformed AS ActionsPerformed, @Message AS Message;
    SELECT * FROM @FailedJobs ORDER BY FailCount DESC;
END;

GO

-- [dbo].[CommandExecute]
CREATE OR ALTER PROCEDURE [dbo].[CommandExecute]

@DatabaseContext nvarchar(max),
@Command nvarchar(max),
@CommandType nvarchar(max),
@Mode int,
@Comment nvarchar(max) = NULL,
@DatabaseName nvarchar(max) = NULL,
@SchemaName nvarchar(max) = NULL,
@ObjectName nvarchar(max) = NULL,
@ObjectType nvarchar(max) = NULL,
@IndexName nvarchar(max) = NULL,
@IndexType int = NULL,
@StatisticsName nvarchar(max) = NULL,
@PartitionNumber int = NULL,
@ExtendedInfo xml = NULL,
@LockMessageSeverity int = 16,
@ExecuteAsUser nvarchar(max) = NULL,
@LogToTable nvarchar(max),
@Execute nvarchar(max)

AS

BEGIN

  ----------------------------------------------------------------------------------------------------
  --// Source:  https://ola.hallengren.com                                                        //--
  --// License: https://ola.hallengren.com/license.html                                           //--
  --// GitHub:  https://github.com/olahallengren/sql-server-maintenance-solution                  //--
  --// Version: 2025-08-23 17:25:24                                                               //--
  ----------------------------------------------------------------------------------------------------

  SET NOCOUNT ON

  DECLARE @StartMessage nvarchar(max)
  DECLARE @EndMessage nvarchar(max)
  DECLARE @ErrorMessage nvarchar(max)
  DECLARE @ErrorMessageOriginal nvarchar(max)
  DECLARE @Severity int

  DECLARE @Errors TABLE (ID int IDENTITY PRIMARY KEY,
                         [Message] nvarchar(max) NOT NULL,
                         Severity int NOT NULL,
                         [State] int)

  DECLARE @CurrentMessage nvarchar(max)
  DECLARE @CurrentSeverity int
  DECLARE @CurrentState int

  DECLARE @sp_executesql nvarchar(max) = QUOTENAME(@DatabaseContext) + '.sys.sp_executesql'

  DECLARE @StartTime datetime2
  DECLARE @EndTime datetime2

  DECLARE @ID int

  DECLARE @Error int = 0
  DECLARE @ReturnCode int = 0

  DECLARE @EmptyLine nvarchar(max) = CHAR(9)

  DECLARE @RevertCommand nvarchar(max)

  ----------------------------------------------------------------------------------------------------
  --// Check core requirements                                                                    //--
  ----------------------------------------------------------------------------------------------------

  IF NOT (SELECT [compatibility_level] FROM sys.databases WHERE [name] = DB_NAME()) >= 90
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The database ' + QUOTENAME(DB_NAME()) + ' has to be in compatibility level 90 or higher.', 16, 1
  END

  IF NOT (SELECT uses_ansi_nulls FROM sys.sql_modules WHERE [object_id] = @@PROCID) = 1
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'ANSI_NULLS has to be set to ON for the stored procedure.', 16, 1
  END

  IF NOT (SELECT uses_quoted_identifier FROM sys.sql_modules WHERE [object_id] = @@PROCID) = 1
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'QUOTED_IDENTIFIER has to be set to ON for the stored procedure.', 16, 1
  END

  IF @LogToTable = 'Y' AND NOT EXISTS (SELECT * FROM sys.objects objects INNER JOIN sys.schemas schemas ON objects.[schema_id] = schemas.[schema_id] WHERE objects.[type] = 'U' AND schemas.[name] = 'dbo' AND objects.[name] = 'CommandLog')
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The table CommandLog is missing. Download https://ola.hallengren.com/scripts/CommandLog.sql.', 16, 1
  END

  ----------------------------------------------------------------------------------------------------
  --// Check input parameters                                                                     //--
  ----------------------------------------------------------------------------------------------------

  IF @DatabaseContext IS NULL OR NOT EXISTS (SELECT * FROM sys.databases WHERE name = @DatabaseContext)
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @DatabaseContext is not supported.', 16, 1
  END

  IF @Command IS NULL OR @Command = ''
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @Command is not supported.', 16, 1
  END

  IF @CommandType IS NULL OR @CommandType = '' OR LEN(@CommandType) > 60
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @CommandType is not supported.', 16, 1
  END

  IF @Mode NOT IN(1,2) OR @Mode IS NULL
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @Mode is not supported.', 16, 1
  END

  IF @LockMessageSeverity NOT IN(10,16) OR @LockMessageSeverity IS NULL
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @LockMessageSeverity is not supported.', 16, 1
  END

  IF LEN(@ExecuteAsUser) > 128
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @ExecuteAsUser is not supported.', 16, 1
  END

  IF @LogToTable NOT IN('Y','N') OR @LogToTable IS NULL
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @LogToTable is not supported.', 16, 1
  END

  IF @Execute NOT IN('Y','N') OR @Execute IS NULL
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @Execute is not supported.', 16, 1
  END

  ----------------------------------------------------------------------------------------------------
  --// Raise errors                                                                               //--
  ----------------------------------------------------------------------------------------------------

  DECLARE ErrorCursor CURSOR FAST_FORWARD FOR SELECT [Message], Severity, [State] FROM @Errors ORDER BY [ID] ASC

  OPEN ErrorCursor

  FETCH ErrorCursor INTO @CurrentMessage, @CurrentSeverity, @CurrentState

  WHILE @@FETCH_STATUS = 0
  BEGIN
    RAISERROR('%s', @CurrentSeverity, @CurrentState, @CurrentMessage) WITH NOWAIT
    RAISERROR(@EmptyLine, 10, 1) WITH NOWAIT

    FETCH NEXT FROM ErrorCursor INTO @CurrentMessage, @CurrentSeverity, @CurrentState
  END

  CLOSE ErrorCursor

  DEALLOCATE ErrorCursor

  IF EXISTS (SELECT * FROM @Errors WHERE Severity >= 16)
  BEGIN
    SET @ReturnCode = 50000
    GOTO ReturnCode
  END

  ----------------------------------------------------------------------------------------------------
  --// Execute as user                                                                            //--
  ----------------------------------------------------------------------------------------------------

  IF @ExecuteAsUser IS NOT NULL
  BEGIN
    SET @Command = 'EXECUTE AS USER = ''' + REPLACE(@ExecuteAsUser,'''','''''') + '''; ' + @Command + '; REVERT;'

    SET @RevertCommand = 'REVERT'
  END

  ----------------------------------------------------------------------------------------------------
  --// Log initial information                                                                    //--
  ----------------------------------------------------------------------------------------------------

  SET @StartTime = SYSDATETIME()

  SET @StartMessage = 'Date and time: ' + CONVERT(nvarchar,@StartTime,120)
  RAISERROR('%s',10,1,@StartMessage) WITH NOWAIT

  SET @StartMessage = 'Database context: ' + QUOTENAME(@DatabaseContext)
  RAISERROR('%s',10,1,@StartMessage) WITH NOWAIT

  SET @StartMessage = 'Command: ' + @Command
  RAISERROR('%s',10,1,@StartMessage) WITH NOWAIT

  IF @Comment IS NOT NULL
  BEGIN
    SET @StartMessage = 'Comment: ' + @Comment
    RAISERROR('%s',10,1,@StartMessage) WITH NOWAIT
  END

  IF @LogToTable = 'Y'
  BEGIN
    INSERT INTO dbo.CommandLog (DatabaseName, SchemaName, ObjectName, ObjectType, IndexName, IndexType, StatisticsName, PartitionNumber, ExtendedInfo, CommandType, Command, StartTime)
    VALUES (@DatabaseName, @SchemaName, @ObjectName, @ObjectType, @IndexName, @IndexType, @StatisticsName, @PartitionNumber, @ExtendedInfo, @CommandType, @Command, @StartTime)
  END

  SET @ID = SCOPE_IDENTITY()

  ----------------------------------------------------------------------------------------------------
  --// Execute command                                                                            //--
  ----------------------------------------------------------------------------------------------------

  IF @Mode = 1 AND @Execute = 'Y'
  BEGIN
    EXECUTE @sp_executesql @stmt = @Command
    SET @Error = @@ERROR
    SET @ReturnCode = @Error
  END

  IF @Mode = 2 AND @Execute = 'Y'
  BEGIN
    BEGIN TRY
      EXECUTE @sp_executesql @stmt = @Command
    END TRY
    BEGIN CATCH
      SET @Error = ERROR_NUMBER()
      SET @ErrorMessageOriginal = ERROR_MESSAGE()

      SET @ErrorMessage = 'Msg ' + CAST(ERROR_NUMBER() AS nvarchar) + ', ' + ISNULL(ERROR_MESSAGE(),'')
      SET @Severity = CASE WHEN ERROR_NUMBER() IN(1205,1222) THEN @LockMessageSeverity ELSE 16 END
      RAISERROR('%s',@Severity,1,@ErrorMessage) WITH NOWAIT

      IF NOT (ERROR_NUMBER() IN(1205,1222) AND @LockMessageSeverity = 10)
      BEGIN
        SET @ReturnCode = ERROR_NUMBER()
      END

      IF @ExecuteAsUser IS NOT NULL
      BEGIN
        EXECUTE @sp_executesql @RevertCommand
      END
    END CATCH
  END

  ----------------------------------------------------------------------------------------------------
  --// Log completing information                                                                 //--
  ----------------------------------------------------------------------------------------------------

  SET @EndTime = SYSDATETIME()

  SET @EndMessage = 'Outcome: ' + CASE WHEN @Execute = 'N' THEN 'Not Executed' WHEN @Error = 0 THEN 'Succeeded' ELSE 'Failed' END
  RAISERROR('%s',10,1,@EndMessage) WITH NOWAIT

  SET @EndMessage = 'Duration: ' + CASE WHEN (DATEDIFF(SECOND,@StartTime,@EndTime) / (24 * 3600)) > 0 THEN CAST((DATEDIFF(SECOND,@StartTime,@EndTime) / (24 * 3600)) AS nvarchar) + '.' ELSE '' END + CONVERT(nvarchar,DATEADD(SECOND,DATEDIFF(SECOND,@StartTime,@EndTime),'1900-01-01'),108)
  RAISERROR('%s',10,1,@EndMessage) WITH NOWAIT

  SET @EndMessage = 'Date and time: ' + CONVERT(nvarchar,@EndTime,120)
  RAISERROR('%s',10,1,@EndMessage) WITH NOWAIT

  RAISERROR(@EmptyLine,10,1) WITH NOWAIT

  IF @LogToTable = 'Y'
  BEGIN
    UPDATE dbo.CommandLog
    SET EndTime = @EndTime,
        ErrorNumber = CASE WHEN @Execute = 'N' THEN NULL ELSE @Error END,
        ErrorMessage = @ErrorMessageOriginal
    WHERE ID = @ID
  END

  ReturnCode:
  IF @ReturnCode <> 0
  BEGIN
    RETURN @ReturnCode
  END

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

END

GO

-- [dbo].[DatabaseBackup]

            CREATE PROCEDURE dbo.DatabaseBackup
            AS
            BEGIN
                -- Disabled for alerting purposes
                RETURN;
            END
        
GO

-- [dbo].[DatabaseIntegrityCheck]
CREATE OR ALTER PROCEDURE [dbo].[DatabaseIntegrityCheck]

@Databases nvarchar(max) = NULL,
@CheckCommands nvarchar(max) = 'CHECKDB',
@PhysicalOnly nvarchar(max) = 'N',
@DataPurity nvarchar(max) = 'N',
@NoIndex nvarchar(max) = 'N',
@ExtendedLogicalChecks nvarchar(max) = 'N',
@NoInformationalMessages nvarchar(max) = 'N',
@TabLock nvarchar(max) = 'N',
@FileGroups nvarchar(max) = NULL,
@Objects nvarchar(max) = NULL,
@MaxDOP int = NULL,
@AvailabilityGroups nvarchar(max) = NULL,
@AvailabilityGroupReplicas nvarchar(max) = 'ALL',
@Updateability nvarchar(max) = 'ALL',
@TimeLimit int = NULL,
@LockTimeout int = NULL,
@LockMessageSeverity int = 16,
@StringDelimiter nvarchar(max) = ',',
@DatabaseOrder nvarchar(max) = NULL,
@DatabasesInParallel nvarchar(max) = 'N',
@LogToTable nvarchar(max) = 'N',
@Execute nvarchar(max) = 'Y'

AS

BEGIN

  ----------------------------------------------------------------------------------------------------
  --// Source:  https://ola.hallengren.com                                                        //--
  --// License: https://ola.hallengren.com/license.html                                           //--
  --// GitHub:  https://github.com/olahallengren/sql-server-maintenance-solution                  //--
  --// Version: 2025-08-23 17:25:24                                                               //--
  ----------------------------------------------------------------------------------------------------

  SET NOCOUNT ON

  DECLARE @StartMessage nvarchar(max)
  DECLARE @EndMessage nvarchar(max)
  DECLARE @DatabaseMessage nvarchar(max)
  DECLARE @ErrorMessage nvarchar(max)
  DECLARE @Severity int

  DECLARE @StartTime datetime2 = SYSDATETIME()
  DECLARE @SchemaName nvarchar(max) = OBJECT_SCHEMA_NAME(@@PROCID)
  DECLARE @ObjectName nvarchar(max) = OBJECT_NAME(@@PROCID)
  DECLARE @VersionTimestamp nvarchar(max) = SUBSTRING(OBJECT_DEFINITION(@@PROCID),CHARINDEX('--// Version: ',OBJECT_DEFINITION(@@PROCID)) + LEN('--// Version: ') + 1, 19)
  DECLARE @Parameters nvarchar(max)

  DECLARE @HostPlatform nvarchar(max)

  DECLARE @QueueID int
  DECLARE @QueueStartTime datetime2

  DECLARE @CurrentDBID int
  DECLARE @CurrentDatabaseName nvarchar(max)

  DECLARE @CurrentDatabase_sp_executesql nvarchar(max)

  DECLARE @CurrentUserAccess nvarchar(max)
  DECLARE @CurrentIsReadOnly bit
  DECLARE @CurrentDatabaseState nvarchar(max)
  DECLARE @CurrentInStandby bit
  DECLARE @CurrentRecoveryModel nvarchar(max)

  DECLARE @CurrentReplicaID uniqueidentifier
  DECLARE @CurrentAvailabilityGroupID uniqueidentifier
  DECLARE @CurrentAvailabilityGroup nvarchar(max)
  DECLARE @CurrentAvailabilityGroupRole nvarchar(max)
  DECLARE @CurrentAvailabilityGroupBackupPreference nvarchar(max)
  DECLARE @CurrentSecondaryRoleAllowConnections nvarchar(max)
  DECLARE @CurrentIsPreferredBackupReplica bit
  DECLARE @CurrentDatabaseMirroringRole nvarchar(max)

  DECLARE @CurrentFGID int
  DECLARE @CurrentFileGroupID int
  DECLARE @CurrentFileGroupName nvarchar(max)
  DECLARE @CurrentFileGroupExists bit

  DECLARE @CurrentOID int
  DECLARE @CurrentSchemaID int
  DECLARE @CurrentSchemaName nvarchar(max)
  DECLARE @CurrentObjectID int
  DECLARE @CurrentObjectName nvarchar(max)
  DECLARE @CurrentObjectType nvarchar(max)
  DECLARE @CurrentObjectExists bit

  DECLARE @CurrentDatabaseContext nvarchar(max)
  DECLARE @CurrentCommand nvarchar(max)
  DECLARE @CurrentCommandOutput int
  DECLARE @CurrentCommandType nvarchar(max)

  DECLARE @Errors TABLE (ID int IDENTITY PRIMARY KEY,
                         [Message] nvarchar(max) NOT NULL,
                         Severity int NOT NULL,
                         [State] int)

  DECLARE @CurrentMessage nvarchar(max)
  DECLARE @CurrentSeverity int
  DECLARE @CurrentState int

  DECLARE @tmpDatabases TABLE (ID int IDENTITY,
                               DatabaseName nvarchar(max),
                               DatabaseType nvarchar(max),
                               AvailabilityGroup bit,
                               [Snapshot] bit,
                               StartPosition int,
                               LastCommandTime datetime2,
                               DatabaseSize bigint,
                               LastGoodCheckDbTime datetime2,
                               [Order] int,
                               Selected bit,
                               Completed bit,
                               PRIMARY KEY(Selected, Completed, [Order], ID))

  DECLARE @tmpAvailabilityGroups TABLE (ID int IDENTITY PRIMARY KEY,
                                        AvailabilityGroupName nvarchar(max),
                                        StartPosition int,
                                        Selected bit)

  DECLARE @tmpDatabasesAvailabilityGroups TABLE (DatabaseName nvarchar(max),
                                                 AvailabilityGroupName nvarchar(max))

  DECLARE @tmpFileGroups TABLE (ID int IDENTITY,
                                FileGroupID int,
                                FileGroupName nvarchar(max),
                                StartPosition int,
                                [Order] int,
                                Selected bit,
                                Completed bit,
                                PRIMARY KEY(Selected, Completed, [Order], ID))

  DECLARE @tmpObjects TABLE (ID int IDENTITY,
                             SchemaID int,
                             SchemaName nvarchar(max),
                             ObjectID int,
                             ObjectName nvarchar(max),
                             ObjectType nvarchar(max),
                             StartPosition int,
                             [Order] int,
                             Selected bit,
                             Completed bit,
                             PRIMARY KEY(Selected, Completed, [Order], ID))

  DECLARE @SelectedDatabases TABLE (DatabaseName nvarchar(max),
                                    DatabaseType nvarchar(max),
                                    AvailabilityGroup nvarchar(max),
                                    StartPosition int,
                                    Selected bit)

  DECLARE @SelectedAvailabilityGroups TABLE (AvailabilityGroupName nvarchar(max),
                                             StartPosition int,
                                             Selected bit)

  DECLARE @SelectedFileGroups TABLE (DatabaseName nvarchar(max),
                                     FileGroupName nvarchar(max),
                                     StartPosition int,
                                     Selected bit)

  DECLARE @SelectedObjects TABLE (DatabaseName nvarchar(max),
                                  SchemaName nvarchar(max),
                                  ObjectName nvarchar(max),
                                  StartPosition int,
                                  Selected bit)

  DECLARE @SelectedCheckCommands TABLE (CheckCommand nvarchar(max))

  DECLARE @Error int = 0
  DECLARE @ReturnCode int = 0

  DECLARE @EmptyLine nvarchar(max) = CHAR(9)

  DECLARE @Version numeric(18,10) = CAST(LEFT(CAST(SERVERPROPERTY('ProductVersion') AS nvarchar(max)),CHARINDEX('.',CAST(SERVERPROPERTY('ProductVersion') AS nvarchar(max))) - 1) + '.' + REPLACE(RIGHT(CAST(SERVERPROPERTY('ProductVersion') AS nvarchar(max)), LEN(CAST(SERVERPROPERTY('ProductVersion') AS nvarchar(max))) - CHARINDEX('.',CAST(SERVERPROPERTY('ProductVersion') AS nvarchar(max)))),'.','') AS numeric(18,10))

  IF @Version >= 14
  BEGIN
    SELECT @HostPlatform = host_platform
    FROM sys.dm_os_host_info
  END
  ELSE
  BEGIN
    SET @HostPlatform = 'Windows'
  END

  DECLARE @AmazonRDS bit = CASE WHEN SERVERPROPERTY('EngineEdition') IN (5, 8) THEN 0 WHEN EXISTS (SELECT * FROM sys.databases WHERE [name] = 'rdsadmin') AND SUSER_SNAME(0x01) = 'rdsa' THEN 1 ELSE 0 END

  ----------------------------------------------------------------------------------------------------
  --// Log initial information                                                                    //--
  ----------------------------------------------------------------------------------------------------

  SET @Parameters = '@Databases = ' + ISNULL('''' + REPLACE(@Databases,'''','''''') + '''','NULL')
  SET @Parameters += ', @CheckCommands = ' + ISNULL('''' + REPLACE(@CheckCommands,'''','''''') + '''','NULL')
  SET @Parameters += ', @PhysicalOnly = ' + ISNULL('''' + REPLACE(@PhysicalOnly,'''','''''') + '''','NULL')
  SET @Parameters += ', @DataPurity = ' + ISNULL('''' + REPLACE(@DataPurity,'''','''''') + '''','NULL')
  SET @Parameters += ', @NoIndex = ' + ISNULL('''' + REPLACE(@NoIndex,'''','''''') + '''','NULL')
  SET @Parameters += ', @ExtendedLogicalChecks = ' + ISNULL('''' + REPLACE(@ExtendedLogicalChecks,'''','''''') + '''','NULL')
  SET @Parameters += ', @NoInformationalMessages = ' + ISNULL('''' + REPLACE(@NoInformationalMessages,'''','''''') + '''','NULL')
  SET @Parameters += ', @TabLock = ' + ISNULL('''' + REPLACE(@TabLock,'''','''''') + '''','NULL')
  SET @Parameters += ', @FileGroups = ' + ISNULL('''' + REPLACE(@FileGroups,'''','''''') + '''','NULL')
  SET @Parameters += ', @Objects = ' + ISNULL('''' + REPLACE(@Objects,'''','''''') + '''','NULL')
  SET @Parameters += ', @MaxDOP = ' + ISNULL(CAST(@MaxDOP AS nvarchar),'NULL')
  SET @Parameters += ', @AvailabilityGroups = ' + ISNULL('''' + REPLACE(@AvailabilityGroups,'''','''''') + '''','NULL')
  SET @Parameters += ', @AvailabilityGroupReplicas = ' + ISNULL('''' + REPLACE(@AvailabilityGroupReplicas,'''','''''') + '''','NULL')
  SET @Parameters += ', @Updateability = ' + ISNULL('''' + REPLACE(@Updateability,'''','''''') + '''','NULL')
  SET @Parameters += ', @TimeLimit = ' + ISNULL(CAST(@TimeLimit AS nvarchar),'NULL')
  SET @Parameters += ', @LockTimeout = ' + ISNULL(CAST(@LockTimeout AS nvarchar),'NULL')
  SET @Parameters += ', @LockMessageSeverity = ' + ISNULL(CAST(@LockMessageSeverity AS nvarchar),'NULL')
  SET @Parameters += ', @StringDelimiter = ' + ISNULL('''' + REPLACE(@StringDelimiter,'''','''''') + '''','NULL')
  SET @Parameters += ', @DatabaseOrder = ' + ISNULL('''' + REPLACE(@DatabaseOrder,'''','''''') + '''','NULL')
  SET @Parameters += ', @DatabasesInParallel = ' + ISNULL('''' + REPLACE(@DatabasesInParallel,'''','''''') + '''','NULL')
  SET @Parameters += ', @LogToTable = ' + ISNULL('''' + REPLACE(@LogToTable,'''','''''') + '''','NULL')
  SET @Parameters += ', @Execute = ' + ISNULL('''' + REPLACE(@Execute,'''','''''') + '''','NULL')

  SET @StartMessage = 'Date and time: ' + CONVERT(nvarchar,@StartTime,120)
  RAISERROR('%s',10,1,@StartMessage) WITH NOWAIT

  SET @StartMessage = 'Server: ' + CAST(SERVERPROPERTY('ServerName') AS nvarchar(max))
  RAISERROR('%s',10,1,@StartMessage) WITH NOWAIT

  SET @StartMessage = 'Version: ' + CAST(SERVERPROPERTY('ProductVersion') AS nvarchar(max))
  RAISERROR('%s',10,1,@StartMessage) WITH NOWAIT

  SET @StartMessage = 'Edition: ' + CAST(SERVERPROPERTY('Edition') AS nvarchar(max))
  RAISERROR('%s',10,1,@StartMessage) WITH NOWAIT

  SET @StartMessage = 'Platform: ' + @HostPlatform
  RAISERROR('%s',10,1,@StartMessage) WITH NOWAIT

  SET @StartMessage = 'Procedure: ' + QUOTENAME(DB_NAME()) + '.' + QUOTENAME(@SchemaName) + '.' + QUOTENAME(@ObjectName)
  RAISERROR('%s',10,1,@StartMessage) WITH NOWAIT

  SET @StartMessage = 'Parameters: ' + @Parameters
  RAISERROR('%s',10,1,@StartMessage) WITH NOWAIT

  SET @StartMessage = 'Version: ' + @VersionTimestamp
  RAISERROR('%s',10,1,@StartMessage) WITH NOWAIT

  SET @StartMessage = 'Source: https://ola.hallengren.com'
  RAISERROR('%s',10,1,@StartMessage) WITH NOWAIT

  RAISERROR(@EmptyLine,10,1) WITH NOWAIT

  ----------------------------------------------------------------------------------------------------
  --// Check core requirements                                                                    //--
  ----------------------------------------------------------------------------------------------------

  IF NOT (SELECT [compatibility_level] FROM sys.databases WHERE [name] = DB_NAME()) >= 90
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT  'The database ' + QUOTENAME(DB_NAME()) + ' has to be in compatibility level 90 or higher.', 16, 1
  END

  IF NOT (SELECT uses_ansi_nulls FROM sys.sql_modules WHERE [object_id] = @@PROCID) = 1
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'ANSI_NULLS has to be set to ON for the stored procedure.', 16, 1
  END

  IF NOT (SELECT uses_quoted_identifier FROM sys.sql_modules WHERE [object_id] = @@PROCID) = 1
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'QUOTED_IDENTIFIER has to be set to ON for the stored procedure.', 16, 1
  END

  IF NOT EXISTS (SELECT * FROM sys.objects objects INNER JOIN sys.schemas schemas ON objects.[schema_id] = schemas.[schema_id] WHERE objects.[type] = 'P' AND schemas.[name] = 'dbo' AND objects.[name] = 'CommandExecute')
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The stored procedure CommandExecute is missing. Download https://ola.hallengren.com/scripts/CommandExecute.sql.', 16, 1
  END

  IF EXISTS (SELECT * FROM sys.objects objects INNER JOIN sys.schemas schemas ON objects.[schema_id] = schemas.[schema_id] WHERE objects.[type] = 'P' AND schemas.[name] = 'dbo' AND objects.[name] = 'CommandExecute' AND OBJECT_DEFINITION(objects.[object_id]) NOT LIKE '%@DatabaseContext%')
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The stored procedure CommandExecute needs to be updated. Download https://ola.hallengren.com/scripts/CommandExecute.sql.', 16, 1
  END

  IF @LogToTable = 'Y' AND NOT EXISTS (SELECT * FROM sys.objects objects INNER JOIN sys.schemas schemas ON objects.[schema_id] = schemas.[schema_id] WHERE objects.[type] = 'U' AND schemas.[name] = 'dbo' AND objects.[name] = 'CommandLog')
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The table CommandLog is missing. Download https://ola.hallengren.com/scripts/CommandLog.sql.', 16, 1
  END

  IF @DatabasesInParallel = 'Y' AND NOT EXISTS (SELECT * FROM sys.objects objects INNER JOIN sys.schemas schemas ON objects.[schema_id] = schemas.[schema_id] WHERE objects.[type] = 'U' AND schemas.[name] = 'dbo' AND objects.[name] = 'Queue')
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The table Queue is missing. Download https://ola.hallengren.com/scripts/Queue.sql.', 16, 1
  END

  IF @DatabasesInParallel = 'Y' AND NOT EXISTS (SELECT * FROM sys.objects objects INNER JOIN sys.schemas schemas ON objects.[schema_id] = schemas.[schema_id] WHERE objects.[type] = 'U' AND schemas.[name] = 'dbo' AND objects.[name] = 'QueueDatabase')
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The table QueueDatabase is missing. Download https://ola.hallengren.com/scripts/QueueDatabase.sql.', 16, 1
  END

  IF @@TRANCOUNT <> 0
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The transaction count is not 0.', 16, 1
  END

  ----------------------------------------------------------------------------------------------------
  --// Select databases                                                                           //--
  ----------------------------------------------------------------------------------------------------

  SET @Databases = REPLACE(@Databases, CHAR(10), '')
  SET @Databases = REPLACE(@Databases, CHAR(13), '')

  WHILE CHARINDEX(@StringDelimiter + ' ', @Databases) > 0 SET @Databases = REPLACE(@Databases, @StringDelimiter + ' ', @StringDelimiter)
  WHILE CHARINDEX(' ' + @StringDelimiter, @Databases) > 0 SET @Databases = REPLACE(@Databases, ' ' + @StringDelimiter, @StringDelimiter)

  SET @Databases = LTRIM(RTRIM(@Databases));

  WITH Databases1 (StartPosition, EndPosition, DatabaseItem) AS
  (
  SELECT 1 AS StartPosition,
         ISNULL(NULLIF(CHARINDEX(@StringDelimiter, @Databases, 1), 0), LEN(@Databases) + 1) AS EndPosition,
         SUBSTRING(@Databases, 1, ISNULL(NULLIF(CHARINDEX(@StringDelimiter, @Databases, 1), 0), LEN(@Databases) + 1) - 1) AS DatabaseItem
  WHERE @Databases IS NOT NULL
  UNION ALL
  SELECT CAST(EndPosition AS int) + 1 AS StartPosition,
         ISNULL(NULLIF(CHARINDEX(@StringDelimiter, @Databases, EndPosition + 1), 0), LEN(@Databases) + 1) AS EndPosition,
         SUBSTRING(@Databases, EndPosition + 1, ISNULL(NULLIF(CHARINDEX(@StringDelimiter, @Databases, EndPosition + 1), 0), LEN(@Databases) + 1) - EndPosition - 1) AS DatabaseItem
  FROM Databases1
  WHERE EndPosition < LEN(@Databases) + 1
  ),
  Databases2 (DatabaseItem, StartPosition, Selected) AS
  (
  SELECT CASE WHEN DatabaseItem LIKE '-%' THEN RIGHT(DatabaseItem,LEN(DatabaseItem) - 1) ELSE DatabaseItem END AS DatabaseItem,
         StartPosition,
         CASE WHEN DatabaseItem LIKE '-%' THEN 0 ELSE 1 END AS Selected
  FROM Databases1
  ),
  Databases3 (DatabaseItem, DatabaseType, AvailabilityGroup, StartPosition, Selected) AS
  (
  SELECT CASE WHEN DatabaseItem IN('ALL_DATABASES','SYSTEM_DATABASES','USER_DATABASES','AVAILABILITY_GROUP_DATABASES') THEN '%' ELSE DatabaseItem END AS DatabaseItem,
         CASE WHEN DatabaseItem = 'SYSTEM_DATABASES' THEN 'S' WHEN DatabaseItem = 'USER_DATABASES' THEN 'U' ELSE NULL END AS DatabaseType,
         CASE WHEN DatabaseItem = 'AVAILABILITY_GROUP_DATABASES' THEN 1 ELSE NULL END AvailabilityGroup,
         StartPosition,
         Selected
  FROM Databases2
  ),
  Databases4 (DatabaseName, DatabaseType, AvailabilityGroup, StartPosition, Selected) AS
  (
  SELECT CASE WHEN LEFT(DatabaseItem,1) = '[' AND RIGHT(DatabaseItem,1) = ']' THEN PARSENAME(DatabaseItem,1) ELSE DatabaseItem END AS DatabaseItem,
         DatabaseType,
         AvailabilityGroup,
         StartPosition,
         Selected
  FROM Databases3
  )
  INSERT INTO @SelectedDatabases (DatabaseName, DatabaseType, AvailabilityGroup, StartPosition, Selected)
  SELECT DatabaseName,
         DatabaseType,
         AvailabilityGroup,
         StartPosition,
         Selected
  FROM Databases4
  OPTION (MAXRECURSION 0)

  IF @Version >= 11 AND SERVERPROPERTY('IsHadrEnabled') = 1
  BEGIN
    INSERT INTO @tmpAvailabilityGroups (AvailabilityGroupName, Selected)
    SELECT name AS AvailabilityGroupName,
           0 AS Selected
    FROM sys.availability_groups

    INSERT INTO @tmpDatabasesAvailabilityGroups (DatabaseName, AvailabilityGroupName)
    SELECT databases.name,
           availability_groups.name
    FROM sys.databases databases
    INNER JOIN sys.availability_replicas availability_replicas ON databases.replica_id = availability_replicas.replica_id
    INNER JOIN sys.availability_groups availability_groups ON availability_replicas.group_id = availability_groups.group_id
  END

  INSERT INTO @tmpDatabases (DatabaseName, DatabaseType, AvailabilityGroup, [Snapshot], [Order], Selected, Completed)
  SELECT [name] AS DatabaseName,
         CASE WHEN name IN('master','msdb','model') OR is_distributor = 1 THEN 'S' ELSE 'U' END AS DatabaseType,
         NULL AS AvailabilityGroup,
         CASE WHEN source_database_id IS NOT NULL THEN 1 ELSE 0 END AS [Snapshot],
         0 AS [Order],
         0 AS Selected,
         0 AS Completed
  FROM sys.databases
  ORDER BY [name] ASC

  UPDATE tmpDatabases
  SET AvailabilityGroup = CASE WHEN EXISTS (SELECT * FROM @tmpDatabasesAvailabilityGroups WHERE DatabaseName = tmpDatabases.DatabaseName) THEN 1 ELSE 0 END
  FROM @tmpDatabases tmpDatabases

  UPDATE tmpDatabases
  SET tmpDatabases.Selected = SelectedDatabases.Selected
  FROM @tmpDatabases tmpDatabases
  INNER JOIN @SelectedDatabases SelectedDatabases
  ON tmpDatabases.DatabaseName LIKE REPLACE(SelectedDatabases.DatabaseName,'_','[_]')
  AND (tmpDatabases.DatabaseType = SelectedDatabases.DatabaseType OR SelectedDatabases.DatabaseType IS NULL)
  AND (tmpDatabases.AvailabilityGroup = SelectedDatabases.AvailabilityGroup OR SelectedDatabases.AvailabilityGroup IS NULL)
  AND NOT ((tmpDatabases.DatabaseName = 'tempdb' OR tmpDatabases.[Snapshot] = 1) AND tmpDatabases.DatabaseName <> SelectedDatabases.DatabaseName)
  WHERE SelectedDatabases.Selected = 1

  UPDATE tmpDatabases
  SET tmpDatabases.Selected = SelectedDatabases.Selected
  FROM @tmpDatabases tmpDatabases
  INNER JOIN @SelectedDatabases SelectedDatabases
  ON tmpDatabases.DatabaseName LIKE REPLACE(SelectedDatabases.DatabaseName,'_','[_]')
  AND (tmpDatabases.DatabaseType = SelectedDatabases.DatabaseType OR SelectedDatabases.DatabaseType IS NULL)
  AND (tmpDatabases.AvailabilityGroup = SelectedDatabases.AvailabilityGroup OR SelectedDatabases.AvailabilityGroup IS NULL)
  AND NOT ((tmpDatabases.DatabaseName = 'tempdb' OR tmpDatabases.[Snapshot] = 1) AND tmpDatabases.DatabaseName <> SelectedDatabases.DatabaseName)
  WHERE SelectedDatabases.Selected = 0

  UPDATE tmpDatabases
  SET tmpDatabases.StartPosition = SelectedDatabases2.StartPosition
  FROM @tmpDatabases tmpDatabases
  INNER JOIN (SELECT tmpDatabases.DatabaseName, MIN(SelectedDatabases.StartPosition) AS StartPosition
              FROM @tmpDatabases tmpDatabases
              INNER JOIN @SelectedDatabases SelectedDatabases
              ON tmpDatabases.DatabaseName LIKE REPLACE(SelectedDatabases.DatabaseName,'_','[_]')
              AND (tmpDatabases.DatabaseType = SelectedDatabases.DatabaseType OR SelectedDatabases.DatabaseType IS NULL)
              AND (tmpDatabases.AvailabilityGroup = SelectedDatabases.AvailabilityGroup OR SelectedDatabases.AvailabilityGroup IS NULL)
              WHERE SelectedDatabases.Selected = 1
              GROUP BY tmpDatabases.DatabaseName) SelectedDatabases2
  ON tmpDatabases.DatabaseName = SelectedDatabases2.DatabaseName

  IF @Databases IS NOT NULL AND (NOT EXISTS(SELECT * FROM @SelectedDatabases) OR EXISTS(SELECT * FROM @SelectedDatabases WHERE DatabaseName IS NULL OR DATALENGTH(DatabaseName) = 0))
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @Databases is not supported.', 16, 1
  END

  ----------------------------------------------------------------------------------------------------
  --// Select availability groups                                                                 //--
  ----------------------------------------------------------------------------------------------------

  IF @AvailabilityGroups IS NOT NULL AND @Version >= 11 AND SERVERPROPERTY('IsHadrEnabled') = 1
  BEGIN

    SET @AvailabilityGroups = REPLACE(@AvailabilityGroups, CHAR(10), '')
    SET @AvailabilityGroups = REPLACE(@AvailabilityGroups, CHAR(13), '')

    WHILE CHARINDEX(@StringDelimiter + ' ', @AvailabilityGroups) > 0 SET @AvailabilityGroups = REPLACE(@AvailabilityGroups, @StringDelimiter + ' ', @StringDelimiter)
    WHILE CHARINDEX(' ' + @StringDelimiter, @AvailabilityGroups) > 0 SET @AvailabilityGroups = REPLACE(@AvailabilityGroups, ' ' + @StringDelimiter, @StringDelimiter)

    SET @AvailabilityGroups = LTRIM(RTRIM(@AvailabilityGroups));

    WITH AvailabilityGroups1 (StartPosition, EndPosition, AvailabilityGroupItem) AS
    (
    SELECT 1 AS StartPosition,
           ISNULL(NULLIF(CHARINDEX(@StringDelimiter, @AvailabilityGroups, 1), 0), LEN(@AvailabilityGroups) + 1) AS EndPosition,
           SUBSTRING(@AvailabilityGroups, 1, ISNULL(NULLIF(CHARINDEX(@StringDelimiter, @AvailabilityGroups, 1), 0), LEN(@AvailabilityGroups) + 1) - 1) AS AvailabilityGroupItem
    WHERE @AvailabilityGroups IS NOT NULL
    UNION ALL
    SELECT CAST(EndPosition AS int) + 1 AS StartPosition,
           ISNULL(NULLIF(CHARINDEX(@StringDelimiter, @AvailabilityGroups, EndPosition + 1), 0), LEN(@AvailabilityGroups) + 1) AS EndPosition,
           SUBSTRING(@AvailabilityGroups, EndPosition + 1, ISNULL(NULLIF(CHARINDEX(@StringDelimiter, @AvailabilityGroups, EndPosition + 1), 0), LEN(@AvailabilityGroups) + 1) - EndPosition - 1) AS AvailabilityGroupItem
    FROM AvailabilityGroups1
    WHERE EndPosition < LEN(@AvailabilityGroups) + 1
    ),
    AvailabilityGroups2 (AvailabilityGroupItem, StartPosition, Selected) AS
    (
    SELECT CASE WHEN AvailabilityGroupItem LIKE '-%' THEN RIGHT(AvailabilityGroupItem,LEN(AvailabilityGroupItem) - 1) ELSE AvailabilityGroupItem END AS AvailabilityGroupItem,
           StartPosition,
           CASE WHEN AvailabilityGroupItem LIKE '-%' THEN 0 ELSE 1 END AS Selected
    FROM AvailabilityGroups1
    ),
    AvailabilityGroups3 (AvailabilityGroupItem, StartPosition, Selected) AS
    (
    SELECT CASE WHEN AvailabilityGroupItem = 'ALL_AVAILABILITY_GROUPS' THEN '%' ELSE AvailabilityGroupItem END AS AvailabilityGroupItem,
           StartPosition,
           Selected
    FROM AvailabilityGroups2
    ),
    AvailabilityGroups4 (AvailabilityGroupName, StartPosition, Selected) AS
    (
    SELECT CASE WHEN LEFT(AvailabilityGroupItem,1) = '[' AND RIGHT(AvailabilityGroupItem,1) = ']' THEN PARSENAME(AvailabilityGroupItem,1) ELSE AvailabilityGroupItem END AS AvailabilityGroupItem,
           StartPosition,
           Selected
    FROM AvailabilityGroups3
    )
    INSERT INTO @SelectedAvailabilityGroups (AvailabilityGroupName, StartPosition, Selected)
    SELECT AvailabilityGroupName, StartPosition, Selected
    FROM AvailabilityGroups4
    OPTION (MAXRECURSION 0)

    UPDATE tmpAvailabilityGroups
    SET tmpAvailabilityGroups.Selected = SelectedAvailabilityGroups.Selected
    FROM @tmpAvailabilityGroups tmpAvailabilityGroups
    INNER JOIN @SelectedAvailabilityGroups SelectedAvailabilityGroups
    ON tmpAvailabilityGroups.AvailabilityGroupName LIKE REPLACE(SelectedAvailabilityGroups.AvailabilityGroupName,'_','[_]')
    WHERE SelectedAvailabilityGroups.Selected = 1

    UPDATE tmpAvailabilityGroups
    SET tmpAvailabilityGroups.Selected = SelectedAvailabilityGroups.Selected
    FROM @tmpAvailabilityGroups tmpAvailabilityGroups
    INNER JOIN @SelectedAvailabilityGroups SelectedAvailabilityGroups
    ON tmpAvailabilityGroups.AvailabilityGroupName LIKE REPLACE(SelectedAvailabilityGroups.AvailabilityGroupName,'_','[_]')
    WHERE SelectedAvailabilityGroups.Selected = 0

    UPDATE tmpAvailabilityGroups
    SET tmpAvailabilityGroups.StartPosition = SelectedAvailabilityGroups2.StartPosition
    FROM @tmpAvailabilityGroups tmpAvailabilityGroups
    INNER JOIN (SELECT tmpAvailabilityGroups.AvailabilityGroupName, MIN(SelectedAvailabilityGroups.StartPosition) AS StartPosition
                FROM @tmpAvailabilityGroups tmpAvailabilityGroups
                INNER JOIN @SelectedAvailabilityGroups SelectedAvailabilityGroups
                ON tmpAvailabilityGroups.AvailabilityGroupName LIKE REPLACE(SelectedAvailabilityGroups.AvailabilityGroupName,'_','[_]')
                WHERE SelectedAvailabilityGroups.Selected = 1
                GROUP BY tmpAvailabilityGroups.AvailabilityGroupName) SelectedAvailabilityGroups2
    ON tmpAvailabilityGroups.AvailabilityGroupName = SelectedAvailabilityGroups2.AvailabilityGroupName

    UPDATE tmpDatabases
    SET tmpDatabases.StartPosition = tmpAvailabilityGroups.StartPosition,
        tmpDatabases.Selected = 1
    FROM @tmpDatabases tmpDatabases
    INNER JOIN @tmpDatabasesAvailabilityGroups tmpDatabasesAvailabilityGroups ON tmpDatabases.DatabaseName = tmpDatabasesAvailabilityGroups.DatabaseName
    INNER JOIN @tmpAvailabilityGroups tmpAvailabilityGroups ON tmpDatabasesAvailabilityGroups.AvailabilityGroupName = tmpAvailabilityGroups.AvailabilityGroupName
    WHERE tmpAvailabilityGroups.Selected = 1

  END

  IF @AvailabilityGroups IS NOT NULL AND (NOT EXISTS(SELECT * FROM @SelectedAvailabilityGroups) OR EXISTS(SELECT * FROM @SelectedAvailabilityGroups WHERE AvailabilityGroupName IS NULL OR AvailabilityGroupName = '') OR @Version < 11 OR SERVERPROPERTY('IsHadrEnabled') = 0)
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @AvailabilityGroups is not supported.', 16, 1
  END

  IF (@Databases IS NULL AND @AvailabilityGroups IS NULL)
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'You need to specify one of the parameters @Databases and @AvailabilityGroups.', 16, 2
  END

  IF (@Databases IS NOT NULL AND @AvailabilityGroups IS NOT NULL)
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'You can only specify one of the parameters @Databases and @AvailabilityGroups.', 16, 3
  END

  ----------------------------------------------------------------------------------------------------
  --// Select filegroups                                                                          //--
  ----------------------------------------------------------------------------------------------------

  SET @FileGroups = REPLACE(@FileGroups, CHAR(10), '')
  SET @FileGroups = REPLACE(@FileGroups, CHAR(13), '')

  WHILE CHARINDEX(@StringDelimiter + ' ', @FileGroups) > 0 SET @FileGroups = REPLACE(@FileGroups, @StringDelimiter + ' ', @StringDelimiter)
  WHILE CHARINDEX(' ' + @StringDelimiter, @FileGroups) > 0 SET @FileGroups = REPLACE(@FileGroups, ' ' + @StringDelimiter, @StringDelimiter)

  SET @FileGroups = LTRIM(RTRIM(@FileGroups));

  WITH FileGroups1 (StartPosition, EndPosition, FileGroupItem) AS
  (
  SELECT 1 AS StartPosition,
         ISNULL(NULLIF(CHARINDEX(@StringDelimiter, @FileGroups, 1), 0), LEN(@FileGroups) + 1) AS EndPosition,
         SUBSTRING(@FileGroups, 1, ISNULL(NULLIF(CHARINDEX(@StringDelimiter, @FileGroups, 1), 0), LEN(@FileGroups) + 1) - 1) AS FileGroupItem
  WHERE @FileGroups IS NOT NULL
  UNION ALL
  SELECT CAST(EndPosition AS int) + 1 AS StartPosition,
         ISNULL(NULLIF(CHARINDEX(@StringDelimiter, @FileGroups, EndPosition + 1), 0), LEN(@FileGroups) + 1) AS EndPosition,
         SUBSTRING(@FileGroups, EndPosition + 1, ISNULL(NULLIF(CHARINDEX(@StringDelimiter, @FileGroups, EndPosition + 1), 0), LEN(@FileGroups) + 1) - EndPosition - 1) AS FileGroupItem
  FROM FileGroups1
  WHERE EndPosition < LEN(@FileGroups) + 1
  ),
  FileGroups2 (FileGroupItem, StartPosition, Selected) AS
  (
  SELECT CASE WHEN FileGroupItem LIKE '-%' THEN RIGHT(FileGroupItem,LEN(FileGroupItem) - 1) ELSE FileGroupItem END AS FileGroupItem,
         StartPosition,
         CASE WHEN FileGroupItem LIKE '-%' THEN 0 ELSE 1 END AS Selected
  FROM FileGroups1
  ),
  FileGroups3 (FileGroupItem, StartPosition, Selected) AS
  (
  SELECT CASE WHEN FileGroupItem = 'ALL_FILEGROUPS' THEN '%.%' ELSE FileGroupItem END AS FileGroupItem,
         StartPosition,
         Selected
  FROM FileGroups2
  ),
  FileGroups4 (DatabaseName, FileGroupName, StartPosition, Selected) AS
  (
  SELECT CASE WHEN PARSENAME(FileGroupItem,4) IS NULL AND PARSENAME(FileGroupItem,3) IS NULL THEN PARSENAME(FileGroupItem,2) ELSE NULL END AS DatabaseName,
         CASE WHEN PARSENAME(FileGroupItem,4) IS NULL AND PARSENAME(FileGroupItem,3) IS NULL THEN PARSENAME(FileGroupItem,1) ELSE NULL END AS FileGroupName,
         StartPosition,
         Selected
  FROM FileGroups3
  )
  INSERT INTO @SelectedFileGroups (DatabaseName, FileGroupName, StartPosition, Selected)
  SELECT DatabaseName, FileGroupName, StartPosition, Selected
  FROM FileGroups4
  OPTION (MAXRECURSION 0)

  ----------------------------------------------------------------------------------------------------
  --// Select objects                                                                             //--
  ----------------------------------------------------------------------------------------------------

  SET @Objects = REPLACE(@Objects, CHAR(10), '')
  SET @Objects = REPLACE(@Objects, CHAR(13), '')

  WHILE CHARINDEX(@StringDelimiter + ' ', @Objects) > 0 SET @Objects = REPLACE(@Objects, @StringDelimiter + ' ', @StringDelimiter)
  WHILE CHARINDEX(' ' + @StringDelimiter, @Objects) > 0 SET @Objects = REPLACE(@Objects, ' ' + @StringDelimiter, @StringDelimiter)

  SET @Objects = LTRIM(RTRIM(@Objects));

  WITH Objects1 (StartPosition, EndPosition, ObjectItem) AS
  (
  SELECT 1 AS StartPosition,
         ISNULL(NULLIF(CHARINDEX(@StringDelimiter, @Objects, 1), 0), LEN(@Objects) + 1) AS EndPosition,
         SUBSTRING(@Objects, 1, ISNULL(NULLIF(CHARINDEX(@StringDelimiter, @Objects, 1), 0), LEN(@Objects) + 1) - 1) AS ObjectItem
  WHERE @Objects IS NOT NULL
  UNION ALL
  SELECT CAST(EndPosition AS int) + 1 AS StartPosition,
         ISNULL(NULLIF(CHARINDEX(@StringDelimiter, @Objects, EndPosition + 1), 0), LEN(@Objects) + 1) AS EndPosition,
         SUBSTRING(@Objects, EndPosition + 1, ISNULL(NULLIF(CHARINDEX(@StringDelimiter, @Objects, EndPosition + 1), 0), LEN(@Objects) + 1) - EndPosition - 1) AS ObjectItem
  FROM Objects1
  WHERE EndPosition < LEN(@Objects) + 1
  ),
  Objects2 (ObjectItem, StartPosition, Selected) AS
  (
  SELECT CASE WHEN ObjectItem LIKE '-%' THEN RIGHT(ObjectItem,LEN(ObjectItem) - 1) ELSE ObjectItem END AS ObjectItem,
          StartPosition,
         CASE WHEN ObjectItem LIKE '-%' THEN 0 ELSE 1 END AS Selected
  FROM Objects1
  ),
  Objects3 (ObjectItem, StartPosition, Selected) AS
  (
  SELECT CASE WHEN ObjectItem = 'ALL_OBJECTS' THEN '%.%.%' ELSE ObjectItem END AS ObjectItem,
         StartPosition,
         Selected
  FROM Objects2
  ),
  Objects4 (DatabaseName, SchemaName, ObjectName, StartPosition, Selected) AS
  (
  SELECT CASE WHEN PARSENAME(ObjectItem,4) IS NULL THEN PARSENAME(ObjectItem,3) ELSE NULL END AS DatabaseName,
         CASE WHEN PARSENAME(ObjectItem,4) IS NULL THEN PARSENAME(ObjectItem,2) ELSE NULL END AS SchemaName,
         CASE WHEN PARSENAME(ObjectItem,4) IS NULL THEN PARSENAME(ObjectItem,1) ELSE NULL END AS ObjectName,
         StartPosition,
         Selected
  FROM Objects3
  )
  INSERT INTO @SelectedObjects (DatabaseName, SchemaName, ObjectName, StartPosition, Selected)
  SELECT DatabaseName, SchemaName, ObjectName, StartPosition, Selected
  FROM Objects4
  OPTION (MAXRECURSION 0)

  ----------------------------------------------------------------------------------------------------
  --// Select check commands                                                                      //--
  ----------------------------------------------------------------------------------------------------

  SET @CheckCommands = REPLACE(@CheckCommands, @StringDelimiter + ' ', @StringDelimiter);

  WITH CheckCommands (StartPosition, EndPosition, CheckCommand) AS
  (
  SELECT 1 AS StartPosition,
         ISNULL(NULLIF(CHARINDEX(@StringDelimiter, @CheckCommands, 1), 0), LEN(@CheckCommands) + 1) AS EndPosition,
         SUBSTRING(@CheckCommands, 1, ISNULL(NULLIF(CHARINDEX(@StringDelimiter, @CheckCommands, 1), 0), LEN(@CheckCommands) + 1) - 1) AS CheckCommand
  WHERE @CheckCommands IS NOT NULL
  UNION ALL
  SELECT CAST(EndPosition AS int) + 1 AS StartPosition,
         ISNULL(NULLIF(CHARINDEX(@StringDelimiter, @CheckCommands, EndPosition + 1), 0), LEN(@CheckCommands) + 1) AS EndPosition,
         SUBSTRING(@CheckCommands, EndPosition + 1, ISNULL(NULLIF(CHARINDEX(@StringDelimiter, @CheckCommands, EndPosition + 1), 0), LEN(@CheckCommands) + 1) - EndPosition - 1) AS CheckCommand
  FROM CheckCommands
  WHERE EndPosition < LEN(@CheckCommands) + 1
  )
  INSERT INTO @SelectedCheckCommands (CheckCommand)
  SELECT CheckCommand
  FROM CheckCommands
  OPTION (MAXRECURSION 0)

  ----------------------------------------------------------------------------------------------------
  --// Check input parameters                                                                     //--
  ----------------------------------------------------------------------------------------------------

  IF EXISTS (SELECT * FROM @SelectedCheckCommands WHERE CheckCommand NOT IN('CHECKDB','CHECKFILEGROUP','CHECKALLOC','CHECKTABLE','CHECKCATALOG'))
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @CheckCommands is not supported.', 16, 1
  END

  IF EXISTS (SELECT * FROM @SelectedCheckCommands GROUP BY CheckCommand HAVING COUNT(*) > 1)
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @CheckCommands is not supported.', 16, 2
  END

  IF NOT EXISTS (SELECT * FROM @SelectedCheckCommands)
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @CheckCommands is not supported.' , 16, 3
  END

  IF EXISTS (SELECT * FROM @SelectedCheckCommands WHERE CheckCommand IN('CHECKDB')) AND EXISTS (SELECT CheckCommand FROM @SelectedCheckCommands WHERE CheckCommand IN('CHECKFILEGROUP','CHECKALLOC','CHECKTABLE','CHECKCATALOG'))
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @CheckCommands is not supported.', 16, 4
  END

  IF EXISTS (SELECT * FROM @SelectedCheckCommands WHERE CheckCommand IN('CHECKFILEGROUP')) AND EXISTS (SELECT CheckCommand FROM @SelectedCheckCommands WHERE CheckCommand IN('CHECKALLOC','CHECKTABLE'))
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @CheckCommands is not supported.', 16, 5
  END

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

  IF @PhysicalOnly NOT IN ('Y','N') OR @PhysicalOnly IS NULL
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @PhysicalOnly is not supported.', 16, 1
  END

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

  IF @DataPurity NOT IN ('Y','N') OR @DataPurity IS NULL
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @DataPurity is not supported.', 16, 1
  END

  IF @PhysicalOnly = 'Y' AND @DataPurity = 'Y'
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The parameters @PhysicalOnly and @DataPurity cannot be used together.', 16, 2
  END

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

  IF @NoIndex NOT IN ('Y','N') OR @NoIndex IS NULL
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @NoIndex is not supported.', 16, 1
  END

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

  IF @ExtendedLogicalChecks NOT IN ('Y','N') OR @ExtendedLogicalChecks IS NULL
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @ExtendedLogicalChecks is not supported.', 16, 1
  END

  IF @PhysicalOnly = 'Y' AND @ExtendedLogicalChecks = 'Y'
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The parameters @PhysicalOnly and @ExtendedLogicalChecks cannot be used together.', 16, 2
  END

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

  IF @NoInformationalMessages NOT IN ('Y','N') OR @NoInformationalMessages IS NULL
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @NoInformationalMessages is not supported.', 16, 1
  END

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

  IF @TabLock NOT IN ('Y','N') OR @TabLock IS NULL
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @TabLock is not supported.', 16, 1
  END

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

  IF EXISTS(SELECT * FROM @SelectedFileGroups WHERE DatabaseName IS NULL OR FileGroupName IS NULL)
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @FileGroups is not supported.', 16, 1
  END

  IF @FileGroups IS NOT NULL AND NOT EXISTS(SELECT * FROM @SelectedFileGroups)
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @FileGroups is not supported.', 16, 2
  END

  IF @FileGroups IS NOT NULL AND NOT EXISTS (SELECT * FROM @SelectedCheckCommands WHERE CheckCommand = 'CHECKFILEGROUP')
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @FileGroups is not supported.', 16, 3
  END

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

  IF EXISTS(SELECT * FROM @SelectedObjects WHERE DatabaseName IS NULL OR SchemaName IS NULL OR ObjectName IS NULL)
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @Objects is not supported.', 16, 1
  END

  IF (@Objects IS NOT NULL AND NOT EXISTS(SELECT * FROM @SelectedObjects))
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @Objects is not supported.', 16, 2
  END

  IF (@Objects IS NOT NULL AND NOT EXISTS (SELECT * FROM @SelectedCheckCommands WHERE CheckCommand = 'CHECKTABLE'))
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @Objects is not supported.', 16, 3
  END

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

  IF @MaxDOP < 0 OR @MaxDOP > 64
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @MaxDOP is not supported.', 16, 1
  END

  IF @MaxDOP IS NOT NULL AND NOT (@Version >= 12.050000 OR SERVERPROPERTY('EngineEdition') IN (5, 8))
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @MaxDOP is not supported. MAXDOP is not available in this version of SQL Server.', 16, 2
  END

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

  IF @AvailabilityGroupReplicas NOT IN('ALL','PRIMARY','SECONDARY','PREFERRED_BACKUP_REPLICA') OR @AvailabilityGroupReplicas IS NULL
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @AvailabilityGroupReplicas is not supported.', 16, 1
  END

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

  IF @Updateability NOT IN('READ_ONLY','READ_WRITE','ALL') OR @Updateability IS NULL
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @Updateability is not supported.', 16, 1
  END

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

  IF @TimeLimit < 0
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @TimeLimit is not supported.', 16, 1
  END

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

  IF @LockTimeout < 0
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @LockTimeout is not supported.', 16, 1
  END

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

  IF @LockMessageSeverity NOT IN(10, 16)
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @LockMessageSeverity is not supported.', 16, 1
  END

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

  IF @StringDelimiter IS NULL OR LEN(@StringDelimiter) > 1
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @StringDelimiter is not supported.', 16, 1
  END

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

  IF @DatabaseOrder NOT IN('DATABASE_NAME_ASC','DATABASE_NAME_DESC','DATABASE_SIZE_ASC','DATABASE_SIZE_DESC','DATABASE_LAST_GOOD_CHECK_ASC','DATABASE_LAST_GOOD_CHECK_DESC','REPLICA_LAST_GOOD_CHECK_ASC','REPLICA_LAST_GOOD_CHECK_DESC')
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @DatabaseOrder is not supported.', 16, 1
  END

  IF @DatabaseOrder IN('DATABASE_LAST_GOOD_CHECK_ASC','DATABASE_LAST_GOOD_CHECK_DESC') AND NOT ((@Version >= 12.06024 AND @Version < 13) OR (@Version >= 13.05026 AND @Version < 14) OR @Version >= 14.0302916 OR SERVERPROPERTY('EngineEdition') = 8)
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @DatabaseOrder is not supported. DATABASEPROPERTYEX(''DatabaseName'', ''LastGoodCheckDbTime'') is not available in this version of SQL Server.', 16, 2
  END

  IF @DatabaseOrder IN('REPLICA_LAST_GOOD_CHECK_ASC','REPLICA_LAST_GOOD_CHECK_DESC') AND @LogToTable = 'N'
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @DatabaseOrder is not supported. You need to provide the parameter @LogToTable = ''Y''.', 16, 3
  END

  IF @DatabaseOrder IN('DATABASE_LAST_GOOD_CHECK_ASC','DATABASE_LAST_GOOD_CHECK_DESC','REPLICA_LAST_GOOD_CHECK_ASC','REPLICA_LAST_GOOD_CHECK_DESC') AND @CheckCommands <> 'CHECKDB'
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @DatabaseOrder is not supported. You need to provide the parameter @CheckCommands = ''CHECKDB''.', 16, 4
  END

  IF @DatabaseOrder IS NOT NULL AND SERVERPROPERTY('EngineEdition') = 5
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @DatabaseOrder is not supported. This parameter is not supported in Azure SQL Database.', 16, 5
  END

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

  IF @DatabasesInParallel NOT IN('Y','N') OR @DatabasesInParallel IS NULL
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @DatabasesInParallel is not supported.', 16, 1
  END

  IF @DatabasesInParallel = 'Y' AND SERVERPROPERTY('EngineEdition') = 5
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @DatabasesInParallel is not supported. This parameter is not supported in Azure SQL Database.', 16, 2
  END

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

  IF @LogToTable NOT IN('Y','N') OR @LogToTable IS NULL
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @LogToTable is not supported.', 16, 1
  END

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

  IF @Execute NOT IN('Y','N') OR @Execute IS NULL
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @Execute is not supported.', 16, 1
  END

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

  IF EXISTS(SELECT * FROM @Errors)
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The documentation is available at https://ola.hallengren.com/sql-server-integrity-check.html.', 16, 1
  END

  ----------------------------------------------------------------------------------------------------
  --// Check that selected databases and availability groups exist                                //--
  ----------------------------------------------------------------------------------------------------

  SET @ErrorMessage = ''
  SELECT @ErrorMessage = @ErrorMessage + QUOTENAME(DatabaseName) + ', '
  FROM @SelectedDatabases
  WHERE DatabaseName NOT LIKE '%[%]%'
  AND DatabaseName NOT IN (SELECT DatabaseName FROM @tmpDatabases)
  IF @@ROWCOUNT > 0
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The following databases in the @Databases parameter do not exist: ' + LEFT(@ErrorMessage,LEN(@ErrorMessage)-1) + '.', 10, 1
  END

  SET @ErrorMessage = ''
  SELECT @ErrorMessage = @ErrorMessage + QUOTENAME(DatabaseName) + ', '
  FROM @SelectedFileGroups
  WHERE DatabaseName NOT LIKE '%[%]%'
  AND DatabaseName NOT IN (SELECT DatabaseName FROM @tmpDatabases)
  IF @@ROWCOUNT > 0
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The following databases in the @FileGroups parameter do not exist: ' + LEFT(@ErrorMessage,LEN(@ErrorMessage)-1) + '.', 10, 1
  END

  SET @ErrorMessage = ''
  SELECT @ErrorMessage = @ErrorMessage + QUOTENAME(DatabaseName) + ', '
  FROM @SelectedObjects
  WHERE DatabaseName NOT LIKE '%[%]%'
  AND DatabaseName NOT IN (SELECT DatabaseName FROM @tmpDatabases)
  IF @@ROWCOUNT > 0
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The following databases in the @Objects parameter do not exist: ' + LEFT(@ErrorMessage,LEN(@ErrorMessage)-1) + '.', 10, 1
  END

  SET @ErrorMessage = ''
  SELECT @ErrorMessage = @ErrorMessage + QUOTENAME(AvailabilityGroupName) + ', '
  FROM @SelectedAvailabilityGroups
  WHERE AvailabilityGroupName NOT LIKE '%[%]%'
  AND AvailabilityGroupName NOT IN (SELECT AvailabilityGroupName FROM @tmpAvailabilityGroups)
  IF @@ROWCOUNT > 0
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The following availability groups do not exist: ' + LEFT(@ErrorMessage,LEN(@ErrorMessage)-1) + '.', 10, 1
  END

  SET @ErrorMessage = ''
  SELECT @ErrorMessage = @ErrorMessage + QUOTENAME(DatabaseName) + ', '
  FROM @SelectedFileGroups
  WHERE DatabaseName NOT LIKE '%[%]%'
  AND DatabaseName IN (SELECT DatabaseName FROM @tmpDatabases)
  AND DatabaseName NOT IN (SELECT DatabaseName FROM @tmpDatabases WHERE Selected = 1)
  IF @@ROWCOUNT > 0
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The following databases have been selected in the @FileGroups parameter, but not in the @Databases or @AvailabilityGroups parameters: ' + LEFT(@ErrorMessage,LEN(@ErrorMessage)-1) + '.', 10, 1
  END

  SET @ErrorMessage = ''
  SELECT @ErrorMessage = @ErrorMessage + QUOTENAME(DatabaseName) + ', '
  FROM @SelectedObjects
  WHERE DatabaseName NOT LIKE '%[%]%'
  AND DatabaseName IN (SELECT DatabaseName FROM @tmpDatabases)
  AND DatabaseName NOT IN (SELECT DatabaseName FROM @tmpDatabases WHERE Selected = 1)
  IF @@ROWCOUNT > 0
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The following databases have been selected in the @Objects parameter, but not in the @Databases or @AvailabilityGroups parameters: ' + LEFT(@ErrorMessage,LEN(@ErrorMessage)-1) + '.', 10, 1
  END

  ----------------------------------------------------------------------------------------------------
  --// Check @@SERVERNAME                                                                         //--
  ----------------------------------------------------------------------------------------------------

  IF UPPER(@@SERVERNAME) <> UPPER(CAST(SERVERPROPERTY('ServerName') AS nvarchar(max))) AND SERVERPROPERTY('IsHadrEnabled') = 1
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The @@SERVERNAME does not match SERVERPROPERTY(''ServerName''). See ' + CASE WHEN SERVERPROPERTY('IsClustered') = 0 THEN 'https://docs.microsoft.com/en-us/sql/database-engine/install-windows/rename-a-computer-that-hosts-a-stand-alone-instance-of-sql-server' WHEN SERVERPROPERTY('IsClustered') = 1 THEN 'https://docs.microsoft.com/en-us/sql/sql-server/failover-clusters/install/rename-a-sql-server-failover-cluster-instance' END + '.', 16, 1
  END

  ----------------------------------------------------------------------------------------------------
  --// Raise errors                                                                               //--
  ----------------------------------------------------------------------------------------------------

  DECLARE ErrorCursor CURSOR FAST_FORWARD FOR SELECT [Message], Severity, [State] FROM @Errors ORDER BY [ID] ASC

  OPEN ErrorCursor

  FETCH ErrorCursor INTO @CurrentMessage, @CurrentSeverity, @CurrentState

  WHILE @@FETCH_STATUS = 0
  BEGIN
    RAISERROR('%s', @CurrentSeverity, @CurrentState, @CurrentMessage) WITH NOWAIT
    RAISERROR(@EmptyLine, 10, 1) WITH NOWAIT

    FETCH NEXT FROM ErrorCursor INTO @CurrentMessage, @CurrentSeverity, @CurrentState
  END

  CLOSE ErrorCursor

  DEALLOCATE ErrorCursor

  IF EXISTS (SELECT * FROM @Errors WHERE Severity >= 16)
  BEGIN
    SET @ReturnCode = 50000
    GOTO Logging
  END

  ----------------------------------------------------------------------------------------------------
  --// Update database order                                                                      //--
  ----------------------------------------------------------------------------------------------------

  IF @DatabaseOrder IN('DATABASE_SIZE_ASC','DATABASE_SIZE_DESC')
  BEGIN
    UPDATE tmpDatabases
    SET DatabaseSize = (SELECT SUM(CAST(size AS bigint)) FROM sys.master_files WHERE [type] = 0 AND database_id = DB_ID(tmpDatabases.DatabaseName))
    FROM @tmpDatabases tmpDatabases
  END

  IF @DatabaseOrder IN('DATABASE_LAST_GOOD_CHECK_ASC','DATABASE_LAST_GOOD_CHECK_DESC')
  BEGIN
    UPDATE tmpDatabases
    SET LastGoodCheckDbTime = NULLIF(CAST(DATABASEPROPERTYEX (DatabaseName,'LastGoodCheckDbTime') AS datetime2),'1900-01-01 00:00:00.000')
    FROM @tmpDatabases tmpDatabases
  END

  IF @DatabaseOrder IN('REPLICA_LAST_GOOD_CHECK_ASC','REPLICA_LAST_GOOD_CHECK_DESC')
  BEGIN
    UPDATE tmpDatabases
    SET LastCommandTime = MaxStartTime
    FROM @tmpDatabases tmpDatabases
    INNER JOIN (SELECT DatabaseName, MAX(StartTime) AS MaxStartTime
                FROM dbo.CommandLog
                WHERE CommandType = 'DBCC_CHECKDB'
                AND ErrorNumber = 0
                GROUP BY DatabaseName) CommandLog
    ON tmpDatabases.DatabaseName = CommandLog.DatabaseName COLLATE DATABASE_DEFAULT
  END

  IF @DatabaseOrder IS NULL
  BEGIN
    WITH tmpDatabases AS (
    SELECT DatabaseName, [Order], ROW_NUMBER() OVER (ORDER BY StartPosition ASC, DatabaseName ASC) AS RowNumber
    FROM @tmpDatabases tmpDatabases
    WHERE Selected = 1
    )
    UPDATE tmpDatabases
    SET [Order] = RowNumber
  END
  ELSE
  IF @DatabaseOrder = 'DATABASE_NAME_ASC'
  BEGIN
    WITH tmpDatabases AS (
    SELECT DatabaseName, [Order], ROW_NUMBER() OVER (ORDER BY DatabaseName ASC) AS RowNumber
    FROM @tmpDatabases tmpDatabases
    WHERE Selected = 1
    )
    UPDATE tmpDatabases
    SET [Order] = RowNumber
  END
  ELSE
  IF @DatabaseOrder = 'DATABASE_NAME_DESC'
  BEGIN
    WITH tmpDatabases AS (
    SELECT DatabaseName, [Order], ROW_NUMBER() OVER (ORDER BY DatabaseName DESC) AS RowNumber
    FROM @tmpDatabases tmpDatabases
    WHERE Selected = 1
    )
    UPDATE tmpDatabases
    SET [Order] = RowNumber
  END
  ELSE
  IF @DatabaseOrder = 'DATABASE_SIZE_ASC'
  BEGIN
    WITH tmpDatabases AS (
    SELECT DatabaseName, [Order], ROW_NUMBER() OVER (ORDER BY DatabaseSize ASC) AS RowNumber
    FROM @tmpDatabases tmpDatabases
    WHERE Selected = 1
    )
    UPDATE tmpDatabases
    SET [Order] = RowNumber
  END
  ELSE
  IF @DatabaseOrder = 'DATABASE_SIZE_DESC'
  BEGIN
    WITH tmpDatabases AS (
    SELECT DatabaseName, [Order], ROW_NUMBER() OVER (ORDER BY DatabaseSize DESC) AS RowNumber
    FROM @tmpDatabases tmpDatabases
    WHERE Selected = 1
    )
    UPDATE tmpDatabases
    SET [Order] = RowNumber
  END
  ELSE
  IF @DatabaseOrder = 'DATABASE_LAST_GOOD_CHECK_ASC'
  BEGIN
    WITH tmpDatabases AS (
    SELECT DatabaseName, [Order], ROW_NUMBER() OVER (ORDER BY LastGoodCheckDbTime ASC) AS RowNumber
    FROM @tmpDatabases tmpDatabases
    WHERE Selected = 1
    )
    UPDATE tmpDatabases
    SET [Order] = RowNumber
  END
  ELSE
  IF @DatabaseOrder = 'DATABASE_LAST_GOOD_CHECK_DESC'
  BEGIN
    WITH tmpDatabases AS (
    SELECT DatabaseName, [Order], ROW_NUMBER() OVER (ORDER BY LastGoodCheckDbTime DESC) AS RowNumber
    FROM @tmpDatabases tmpDatabases
    WHERE Selected = 1
    )
    UPDATE tmpDatabases
    SET [Order] = RowNumber
  END
  ELSE
  IF @DatabaseOrder = 'REPLICA_LAST_GOOD_CHECK_ASC'
  BEGIN
    WITH tmpDatabases AS (
    SELECT DatabaseName, [Order], ROW_NUMBER() OVER (ORDER BY LastCommandTime ASC) AS RowNumber
    FROM @tmpDatabases tmpDatabases
    WHERE Selected = 1
    )
    UPDATE tmpDatabases
    SET [Order] = RowNumber
  END
  ELSE
  IF @DatabaseOrder = 'REPLICA_LAST_GOOD_CHECK_DESC'
  BEGIN
    WITH tmpDatabases AS (
    SELECT DatabaseName, [Order], ROW_NUMBER() OVER (ORDER BY LastCommandTime DESC) AS RowNumber
    FROM @tmpDatabases tmpDatabases
    WHERE Selected = 1
    )
    UPDATE tmpDatabases
    SET [Order] = RowNumber
  END

  ----------------------------------------------------------------------------------------------------
  --// Update the queue                                                                           //--
  ----------------------------------------------------------------------------------------------------

  IF @DatabasesInParallel = 'Y'
  BEGIN

    BEGIN TRY

      SELECT @QueueID = QueueID
      FROM dbo.[Queue]
      WHERE SchemaName = @SchemaName
      AND ObjectName = @ObjectName
      AND [Parameters] = @Parameters

      IF @QueueID IS NULL
      BEGIN
        BEGIN TRANSACTION

        SELECT @QueueID = QueueID
        FROM dbo.[Queue] WITH (UPDLOCK, HOLDLOCK)
        WHERE SchemaName = @SchemaName
        AND ObjectName = @ObjectName
        AND [Parameters] = @Parameters

        IF @QueueID IS NULL
        BEGIN
          INSERT INTO dbo.[Queue] (SchemaName, ObjectName, [Parameters])
          SELECT @SchemaName, @ObjectName, @Parameters

          SET @QueueID = SCOPE_IDENTITY()
        END

        COMMIT TRANSACTION
      END

      BEGIN TRANSACTION

      UPDATE [Queue]
      SET QueueStartTime = SYSDATETIME(),
          SessionID = @@SPID,
          RequestID = (SELECT request_id FROM sys.dm_exec_requests WHERE session_id = @@SPID),
          RequestStartTime = (SELECT start_time FROM sys.dm_exec_requests WHERE session_id = @@SPID)
      FROM dbo.[Queue] [Queue]
      WHERE QueueID = @QueueID
      AND NOT EXISTS (SELECT *
                      FROM sys.dm_exec_requests
                      WHERE session_id = [Queue].SessionID
                      AND request_id = [Queue].RequestID
                      AND start_time = [Queue].RequestStartTime)
      AND NOT EXISTS (SELECT *
                      FROM dbo.QueueDatabase QueueDatabase
                      INNER JOIN sys.dm_exec_requests ON QueueDatabase.SessionID = session_id AND QueueDatabase.RequestID = request_id AND QueueDatabase.RequestStartTime = start_time
                      WHERE QueueDatabase.QueueID = @QueueID)

      IF @@ROWCOUNT = 1
      BEGIN
        INSERT INTO dbo.QueueDatabase (QueueID, DatabaseName)
        SELECT @QueueID AS QueueID,
               DatabaseName
        FROM @tmpDatabases tmpDatabases
        WHERE Selected = 1
        AND NOT EXISTS (SELECT * FROM dbo.QueueDatabase WHERE DatabaseName = tmpDatabases.DatabaseName AND QueueID = @QueueID)

        DELETE QueueDatabase
        FROM dbo.QueueDatabase QueueDatabase
        WHERE QueueID = @QueueID
        AND NOT EXISTS (SELECT * FROM @tmpDatabases tmpDatabases WHERE DatabaseName = QueueDatabase.DatabaseName AND Selected = 1)

        UPDATE QueueDatabase
        SET DatabaseOrder = tmpDatabases.[Order]
        FROM dbo.QueueDatabase QueueDatabase
        INNER JOIN @tmpDatabases tmpDatabases ON QueueDatabase.DatabaseName = tmpDatabases.DatabaseName
        WHERE QueueID = @QueueID
      END

      COMMIT TRANSACTION

      SELECT @QueueStartTime = QueueStartTime
      FROM dbo.[Queue]
      WHERE QueueID = @QueueID

    END TRY

    BEGIN CATCH
      IF XACT_STATE() <> 0
      BEGIN
        ROLLBACK TRANSACTION
      END
      SET @ErrorMessage = 'Msg ' + CAST(ERROR_NUMBER() AS nvarchar) + ', ' + ISNULL(ERROR_MESSAGE(),'')
      RAISERROR('%s',16,1,@ErrorMessage) WITH NOWAIT
      RAISERROR(@EmptyLine,10,1) WITH NOWAIT
      SET @ReturnCode = ERROR_NUMBER()
      GOTO Logging
    END CATCH

  END

  ----------------------------------------------------------------------------------------------------
  --// Execute commands                                                                           //--
  ----------------------------------------------------------------------------------------------------

  WHILE (1 = 1)
  BEGIN

    IF @DatabasesInParallel = 'Y'
    BEGIN
      UPDATE QueueDatabase
      SET DatabaseStartTime = NULL,
          SessionID = NULL,
          RequestID = NULL,
          RequestStartTime = NULL
      FROM dbo.QueueDatabase QueueDatabase
      WHERE QueueID = @QueueID
      AND DatabaseStartTime IS NOT NULL
      AND DatabaseEndTime IS NULL
      AND NOT EXISTS (SELECT * FROM sys.dm_exec_requests WHERE session_id = QueueDatabase.SessionID AND request_id = QueueDatabase.RequestID AND start_time = QueueDatabase.RequestStartTime)

      UPDATE QueueDatabase
      SET DatabaseStartTime = SYSDATETIME(),
          DatabaseEndTime = NULL,
          SessionID = @@SPID,
          RequestID = (SELECT request_id FROM sys.dm_exec_requests WHERE session_id = @@SPID),
          RequestStartTime = (SELECT start_time FROM sys.dm_exec_requests WHERE session_id = @@SPID),
          @CurrentDatabaseName = DatabaseName
      FROM (SELECT TOP 1 DatabaseStartTime,
                         DatabaseEndTime,
                         SessionID,
                         RequestID,
                         RequestStartTime,
                         DatabaseName
            FROM dbo.QueueDatabase
            WHERE QueueID = @QueueID
            AND (DatabaseStartTime < @QueueStartTime OR DatabaseStartTime IS NULL)
            AND NOT (DatabaseStartTime IS NOT NULL AND DatabaseEndTime IS NULL)
            ORDER BY DatabaseOrder ASC
            ) QueueDatabase
    END
    ELSE
    BEGIN
      SELECT TOP 1 @CurrentDBID = ID,
                   @CurrentDatabaseName = DatabaseName
      FROM @tmpDatabases
      WHERE Selected = 1
      AND Completed = 0
      ORDER BY [Order] ASC
    END

    IF @@ROWCOUNT = 0
    BEGIN
     BREAK
    END

    SET @CurrentDatabase_sp_executesql = QUOTENAME(@CurrentDatabaseName) + '.sys.sp_executesql'

    BEGIN
      SET @DatabaseMessage = 'Date and time: ' + CONVERT(nvarchar,SYSDATETIME(),120)
      RAISERROR('%s',10,1,@DatabaseMessage) WITH NOWAIT

      SET @DatabaseMessage = 'Database: ' + QUOTENAME(@CurrentDatabaseName)
      RAISERROR('%s',10,1,@DatabaseMessage) WITH NOWAIT
    END

    SELECT @CurrentUserAccess = user_access_desc,
           @CurrentIsReadOnly = is_read_only,
           @CurrentDatabaseState = state_desc,
           @CurrentInStandby = is_in_standby,
           @CurrentRecoveryModel = recovery_model_desc
    FROM sys.databases
    WHERE [name] = @CurrentDatabaseName

    BEGIN
      SET @DatabaseMessage = 'State: ' + @CurrentDatabaseState
      RAISERROR('%s',10,1,@DatabaseMessage) WITH NOWAIT

      SET @DatabaseMessage = 'Standby: ' + CASE WHEN @CurrentInStandby = 1 THEN 'Yes' ELSE 'No' END
      RAISERROR('%s',10,1,@DatabaseMessage) WITH NOWAIT

      SET @DatabaseMessage = 'Updateability: ' + CASE WHEN @CurrentIsReadOnly = 1 THEN 'READ_ONLY' WHEN  @CurrentIsReadOnly = 0 THEN 'READ_WRITE' END
      RAISERROR('%s',10,1,@DatabaseMessage) WITH NOWAIT

      SET @DatabaseMessage = 'User access: ' + @CurrentUserAccess
      RAISERROR('%s',10,1,@DatabaseMessage) WITH NOWAIT

      SET @DatabaseMessage = 'Recovery model: ' + @CurrentRecoveryModel
      RAISERROR('%s',10,1,@DatabaseMessage) WITH NOWAIT
    END

    IF @Version >= 11 AND SERVERPROPERTY('IsHadrEnabled') = 1
    BEGIN
      SELECT @CurrentReplicaID = databases.replica_id
      FROM sys.databases databases
      INNER JOIN sys.availability_replicas availability_replicas ON databases.replica_id = availability_replicas.replica_id
      WHERE databases.[name] = @CurrentDatabaseName

      SELECT @CurrentAvailabilityGroupID = group_id,
             @CurrentSecondaryRoleAllowConnections = secondary_role_allow_connections_desc
      FROM sys.availability_replicas
      WHERE replica_id = @CurrentReplicaID

      SELECT @CurrentAvailabilityGroupRole = role_desc
      FROM sys.dm_hadr_availability_replica_states
      WHERE replica_id = @CurrentReplicaID

      SELECT @CurrentAvailabilityGroup = [name],
             @CurrentAvailabilityGroupBackupPreference = UPPER(automated_backup_preference_desc)
      FROM sys.availability_groups
      WHERE group_id = @CurrentAvailabilityGroupID
    END

    IF @Version >= 11 AND SERVERPROPERTY('IsHadrEnabled') = 1 AND @CurrentAvailabilityGroup IS NOT NULL AND @AvailabilityGroupReplicas = 'PREFERRED_BACKUP_REPLICA'
    BEGIN
      SELECT @CurrentIsPreferredBackupReplica = sys.fn_hadr_backup_is_preferred_replica(@CurrentDatabaseName)
    END

    IF SERVERPROPERTY('EngineEdition') <> 5
    BEGIN
      SELECT @CurrentDatabaseMirroringRole = UPPER(mirroring_role_desc)
      FROM sys.database_mirroring database_mirroring
      INNER JOIN sys.databases databases ON database_mirroring.database_id = databases.database_id
      WHERE databases.[name] = @CurrentDatabaseName
    END

    IF @CurrentAvailabilityGroup IS NOT NULL
    BEGIN
      SET @DatabaseMessage =  'Availability group: ' + ISNULL(@CurrentAvailabilityGroup,'N/A')
      RAISERROR('%s',10,1,@DatabaseMessage) WITH NOWAIT

      SET @DatabaseMessage = 'Availability group role: ' + ISNULL(@CurrentAvailabilityGroupRole,'N/A')
      RAISERROR('%s',10,1,@DatabaseMessage) WITH NOWAIT

      IF @CurrentAvailabilityGroupRole = 'SECONDARY'
      BEGIN
        SET @DatabaseMessage =  'Readable Secondary: ' + ISNULL(@CurrentSecondaryRoleAllowConnections,'N/A')
        RAISERROR('%s',10,1,@DatabaseMessage) WITH NOWAIT
      END

      IF @AvailabilityGroupReplicas = 'PREFERRED_BACKUP_REPLICA'
      BEGIN
        SET @DatabaseMessage = 'Availability group backup preference: ' + ISNULL(@CurrentAvailabilityGroupBackupPreference,'N/A')
        RAISERROR('%s',10,1,@DatabaseMessage) WITH NOWAIT

        SET @DatabaseMessage = 'Is preferred backup replica: ' + CASE WHEN @CurrentIsPreferredBackupReplica = 1 THEN 'Yes' WHEN @CurrentIsPreferredBackupReplica = 0 THEN 'No' ELSE 'N/A' END
        RAISERROR('%s',10,1,@DatabaseMessage) WITH NOWAIT
      END
    END

    IF @CurrentDatabaseMirroringRole IS NOT NULL
    BEGIN
      SET @DatabaseMessage = 'Database mirroring role: ' + @CurrentDatabaseMirroringRole
      RAISERROR('%s',10,1,@DatabaseMessage) WITH NOWAIT
    END

    RAISERROR(@EmptyLine,10,1) WITH NOWAIT

    IF @CurrentDatabaseState IN('ONLINE','EMERGENCY')
    AND NOT (@CurrentUserAccess = 'SINGLE_USER')
    AND (@CurrentAvailabilityGroupRole = 'PRIMARY' OR @CurrentAvailabilityGroupRole IS NULL OR SERVERPROPERTY('EngineEdition') = 3)
    AND ((@AvailabilityGroupReplicas = 'PRIMARY' AND @CurrentAvailabilityGroupRole = 'PRIMARY') OR (@AvailabilityGroupReplicas = 'SECONDARY' AND @CurrentAvailabilityGroupRole = 'SECONDARY') OR (@AvailabilityGroupReplicas = 'PREFERRED_BACKUP_REPLICA' AND @CurrentIsPreferredBackupReplica = 1) OR @AvailabilityGroupReplicas = 'ALL' OR @CurrentAvailabilityGroupRole IS NULL)
    AND NOT (@CurrentIsReadOnly = 1 AND @Updateability = 'READ_WRITE')
    AND NOT (@CurrentIsReadOnly = 0 AND @Updateability = 'READ_ONLY')
    AND NOT (@AmazonRDS = 1 AND @CurrentDatabaseName = 'rdsadmin')
    BEGIN

      -- Check database
      IF EXISTS(SELECT * FROM @SelectedCheckCommands WHERE CheckCommand = 'CHECKDB') AND (SYSDATETIME() < DATEADD(SECOND,@TimeLimit,@StartTime) OR @TimeLimit IS NULL)
      BEGIN
        SET @CurrentDatabaseContext = CASE WHEN SERVERPROPERTY('EngineEdition') = 5 THEN @CurrentDatabaseName ELSE 'master' END

        SET @CurrentCommandType = 'DBCC_CHECKDB'

        SET @CurrentCommand = ''
        IF @LockTimeout IS NOT NULL SET @CurrentCommand = 'SET LOCK_TIMEOUT ' + CAST(@LockTimeout * 1000 AS nvarchar) + '; '
        SET @CurrentCommand += 'DBCC CHECKDB (' + QUOTENAME(@CurrentDatabaseName)
        IF @NoIndex = 'Y' SET @CurrentCommand += ', NOINDEX'
        SET @CurrentCommand += ') WITH ALL_ERRORMSGS'
        IF @DataPurity = 'Y' SET @CurrentCommand += ', DATA_PURITY'
        IF @PhysicalOnly = 'Y' SET @CurrentCommand += ', PHYSICAL_ONLY'
        IF @ExtendedLogicalChecks = 'Y' SET @CurrentCommand += ', EXTENDED_LOGICAL_CHECKS'
        IF @NoInformationalMessages = 'Y' SET @CurrentCommand += ', NO_INFOMSGS'
        IF @TabLock = 'Y' SET @CurrentCommand += ', TABLOCK'
        IF @MaxDOP IS NOT NULL SET @CurrentCommand += ', MAXDOP = ' + CAST(@MaxDOP AS nvarchar)

        EXECUTE @CurrentCommandOutput = dbo.CommandExecute @DatabaseContext = @CurrentDatabaseContext, @Command = @CurrentCommand, @CommandType = @CurrentCommandType, @Mode = 1, @DatabaseName = @CurrentDatabaseName, @LogToTable = @LogToTable, @Execute = @Execute
        SET @Error = @@ERROR
        IF @Error <> 0 SET @CurrentCommandOutput = @Error
        IF @CurrentCommandOutput <> 0 SET @ReturnCode = @CurrentCommandOutput
      END

      -- Check filegroups
      IF EXISTS(SELECT * FROM @SelectedCheckCommands WHERE CheckCommand = 'CHECKFILEGROUP')
      AND (@CurrentAvailabilityGroupRole = 'PRIMARY' OR (@CurrentAvailabilityGroupRole = 'SECONDARY' AND @CurrentSecondaryRoleAllowConnections = 'ALL') OR @CurrentAvailabilityGroupRole IS NULL)
      AND (SYSDATETIME() < DATEADD(SECOND,@TimeLimit,@StartTime) OR @TimeLimit IS NULL)
      BEGIN
        SET @CurrentCommand = 'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; SELECT data_space_id AS FileGroupID, name AS FileGroupName, 0 AS [Order], 0 AS Selected, 0 AS Completed FROM sys.filegroups filegroups WHERE [type] <> ''FX'' ORDER BY CASE WHEN filegroups.name = ''PRIMARY'' THEN 1 ELSE 0 END DESC, filegroups.name ASC'

        INSERT INTO @tmpFileGroups (FileGroupID, FileGroupName, [Order], Selected, Completed)
        EXECUTE @CurrentDatabase_sp_executesql  @stmt = @CurrentCommand
        SET @Error = @@ERROR
        IF @Error <> 0 SET @ReturnCode = @Error

        IF @FileGroups IS NULL
        BEGIN
          UPDATE tmpFileGroups
          SET tmpFileGroups.Selected = 1
          FROM @tmpFileGroups tmpFileGroups
        END
        ELSE
        BEGIN
          UPDATE tmpFileGroups
          SET tmpFileGroups.Selected = SelectedFileGroups.Selected
          FROM @tmpFileGroups tmpFileGroups
          INNER JOIN @SelectedFileGroups SelectedFileGroups
          ON @CurrentDatabaseName LIKE REPLACE(SelectedFileGroups.DatabaseName,'_','[_]') AND tmpFileGroups.FileGroupName LIKE REPLACE(SelectedFileGroups.FileGroupName,'_','[_]')
          WHERE SelectedFileGroups.Selected = 1

          UPDATE tmpFileGroups
          SET tmpFileGroups.Selected = SelectedFileGroups.Selected
          FROM @tmpFileGroups tmpFileGroups
          INNER JOIN @SelectedFileGroups SelectedFileGroups
          ON @CurrentDatabaseName LIKE REPLACE(SelectedFileGroups.DatabaseName,'_','[_]') AND tmpFileGroups.FileGroupName LIKE REPLACE(SelectedFileGroups.FileGroupName,'_','[_]')
          WHERE SelectedFileGroups.Selected = 0

          UPDATE tmpFileGroups
          SET tmpFileGroups.StartPosition = SelectedFileGroups2.StartPosition
          FROM @tmpFileGroups tmpFileGroups
          INNER JOIN (SELECT tmpFileGroups.FileGroupName, MIN(SelectedFileGroups.StartPosition) AS StartPosition
                      FROM @tmpFileGroups tmpFileGroups
                      INNER JOIN @SelectedFileGroups SelectedFileGroups
                      ON @CurrentDatabaseName LIKE REPLACE(SelectedFileGroups.DatabaseName,'_','[_]') AND tmpFileGroups.FileGroupName LIKE REPLACE(SelectedFileGroups.FileGroupName,'_','[_]')
                      WHERE SelectedFileGroups.Selected = 1
                      GROUP BY tmpFileGroups.FileGroupName) SelectedFileGroups2
          ON tmpFileGroups.FileGroupName = SelectedFileGroups2.FileGroupName
        END;

        WITH tmpFileGroups AS (
        SELECT FileGroupName, [Order], ROW_NUMBER() OVER (ORDER BY StartPosition ASC, FileGroupName ASC) AS RowNumber
        FROM @tmpFileGroups tmpFileGroups
        WHERE Selected = 1
        )
        UPDATE tmpFileGroups
        SET [Order] = RowNumber

        SET @ErrorMessage = ''
        SELECT @ErrorMessage = @ErrorMessage + QUOTENAME(DatabaseName) + '.' + QUOTENAME(FileGroupName) + ', '
        FROM @SelectedFileGroups SelectedFileGroups
        WHERE DatabaseName = @CurrentDatabaseName
        AND FileGroupName NOT LIKE '%[%]%'
        AND NOT EXISTS (SELECT * FROM @tmpFileGroups WHERE FileGroupName = SelectedFileGroups.FileGroupName)
        IF @@ROWCOUNT > 0
        BEGIN
          SET @ErrorMessage = 'The following file groups do not exist: ' + LEFT(@ErrorMessage,LEN(@ErrorMessage)-1) + '.'
          RAISERROR('%s',10,1,@ErrorMessage) WITH NOWAIT
          SET @Error = @@ERROR
          RAISERROR(@EmptyLine,10,1) WITH NOWAIT
        END

        WHILE (SYSDATETIME() < DATEADD(SECOND,@TimeLimit,@StartTime) OR @TimeLimit IS NULL)
        BEGIN
          SELECT TOP 1 @CurrentFGID = ID,
                       @CurrentFileGroupID = FileGroupID,
                       @CurrentFileGroupName = FileGroupName
          FROM @tmpFileGroups
          WHERE Selected = 1
          AND Completed = 0
          ORDER BY [Order] ASC

          IF @@ROWCOUNT = 0
          BEGIN
            BREAK
          END

          -- Does the filegroup exist?
          SET @CurrentCommand = ''
          IF @LockTimeout IS NOT NULL SET @CurrentCommand = 'SET LOCK_TIMEOUT ' + CAST(@LockTimeout * 1000 AS nvarchar) + '; '
          SET @CurrentCommand += 'IF EXISTS(SELECT * FROM sys.filegroups filegroups WHERE [type] <> ''FX'' AND filegroups.data_space_id = @ParamFileGroupID AND filegroups.[name] = @ParamFileGroupName) BEGIN SET @ParamFileGroupExists = 1 END'

          BEGIN TRY
            EXECUTE @CurrentDatabase_sp_executesql @stmt = @CurrentCommand, @params = N'@ParamFileGroupID int, @ParamFileGroupName sysname, @ParamFileGroupExists bit OUTPUT', @ParamFileGroupID = @CurrentFileGroupID, @ParamFileGroupName = @CurrentFileGroupName, @ParamFileGroupExists = @CurrentFileGroupExists OUTPUT

            IF @CurrentFileGroupExists IS NULL SET @CurrentFileGroupExists = 0
          END TRY
          BEGIN CATCH
            SET @ErrorMessage = 'Msg ' + CAST(ERROR_NUMBER() AS nvarchar) + ', ' + ISNULL(ERROR_MESSAGE(),'') + CASE WHEN ERROR_NUMBER() = 1222 THEN ', ' + ' The file group ' + QUOTENAME(@CurrentFileGroupName) + ' in the database ' + QUOTENAME(@CurrentDatabaseName) + ' is locked. It could not be checked if the filegroup exists.' ELSE '' END
            SET @Severity = CASE WHEN ERROR_NUMBER() IN(1205,1222) THEN @LockMessageSeverity ELSE 16 END
            RAISERROR('%s',@Severity,1,@ErrorMessage) WITH NOWAIT
            RAISERROR(@EmptyLine,10,1) WITH NOWAIT

            IF NOT (ERROR_NUMBER() IN(1205,1222) AND @LockMessageSeverity = 10)
            BEGIN
              SET @ReturnCode = ERROR_NUMBER()
            END
          END CATCH

          IF @CurrentFileGroupExists = 1
          BEGIN
            SET @CurrentDatabaseContext = @CurrentDatabaseName

            SET @CurrentCommandType = 'DBCC_CHECKFILEGROUP'

            SET @CurrentCommand = ''
            IF @LockTimeout IS NOT NULL SET @CurrentCommand = 'SET LOCK_TIMEOUT ' + CAST(@LockTimeout * 1000 AS nvarchar) + '; '
            SET @CurrentCommand += 'DBCC CHECKFILEGROUP (' + QUOTENAME(@CurrentFileGroupName)
            IF @NoIndex = 'Y' SET @CurrentCommand += ', NOINDEX'
            SET @CurrentCommand += ') WITH ALL_ERRORMSGS'
            IF @PhysicalOnly = 'Y' SET @CurrentCommand += ', PHYSICAL_ONLY'
            IF @NoInformationalMessages = 'Y' SET @CurrentCommand += ', NO_INFOMSGS'
            IF @TabLock = 'Y' SET @CurrentCommand += ', TABLOCK'
            IF @MaxDOP IS NOT NULL SET @CurrentCommand += ', MAXDOP = ' + CAST(@MaxDOP AS nvarchar)

            EXECUTE @CurrentCommandOutput = dbo.CommandExecute @DatabaseContext = @CurrentDatabaseContext, @Command = @CurrentCommand, @CommandType = @CurrentCommandType, @Mode = 1, @DatabaseName = @CurrentDatabaseName, @LogToTable = @LogToTable, @Execute = @Execute
            SET @Error = @@ERROR
            IF @Error <> 0 SET @CurrentCommandOutput = @Error
            IF @CurrentCommandOutput <> 0 SET @ReturnCode = @CurrentCommandOutput
          END

          UPDATE @tmpFileGroups
          SET Completed = 1
          WHERE Selected = 1
          AND Completed = 0
          AND ID = @CurrentFGID

          SET @CurrentFGID = NULL
          SET @CurrentFileGroupID = NULL
          SET @CurrentFileGroupName = NULL
          SET @CurrentFileGroupExists = NULL

          SET @CurrentDatabaseContext = NULL
          SET @CurrentCommand = NULL
          SET @CurrentCommandOutput = NULL
          SET @CurrentCommandType = NULL
        END
      END

      -- Check disk space allocation structures
      IF EXISTS(SELECT * FROM @SelectedCheckCommands WHERE CheckCommand = 'CHECKALLOC') AND (SYSDATETIME() < DATEADD(SECOND,@TimeLimit,@StartTime) OR @TimeLimit IS NULL)
      BEGIN
        SET @CurrentDatabaseContext = CASE WHEN SERVERPROPERTY('EngineEdition') = 5 THEN @CurrentDatabaseName ELSE 'master' END

        SET @CurrentCommandType = 'DBCC_CHECKALLOC'

        SET @CurrentCommand = ''
        IF @LockTimeout IS NOT NULL SET @CurrentCommand = 'SET LOCK_TIMEOUT ' + CAST(@LockTimeout * 1000 AS nvarchar) + '; '
        SET @CurrentCommand += 'DBCC CHECKALLOC (' + QUOTENAME(@CurrentDatabaseName)
        SET @CurrentCommand += ') WITH ALL_ERRORMSGS'
        IF @NoInformationalMessages = 'Y' SET @CurrentCommand += ', NO_INFOMSGS'
        IF @TabLock = 'Y' SET @CurrentCommand += ', TABLOCK'

        EXECUTE @CurrentCommandOutput = dbo.CommandExecute @DatabaseContext = @CurrentDatabaseContext, @Command = @CurrentCommand, @CommandType = @CurrentCommandType, @Mode = 1, @DatabaseName = @CurrentDatabaseName, @LogToTable = @LogToTable, @Execute = @Execute
        SET @Error = @@ERROR
        IF @Error <> 0 SET @CurrentCommandOutput = @Error
        IF @CurrentCommandOutput <> 0 SET @ReturnCode = @CurrentCommandOutput
      END

      -- Check objects
      IF EXISTS(SELECT * FROM @SelectedCheckCommands WHERE CheckCommand = 'CHECKTABLE')
      AND (@CurrentAvailabilityGroupRole = 'PRIMARY' OR (@CurrentAvailabilityGroupRole = 'SECONDARY' AND @CurrentSecondaryRoleAllowConnections = 'ALL') OR @CurrentAvailabilityGroupRole IS NULL)
      AND (SYSDATETIME() < DATEADD(SECOND,@TimeLimit,@StartTime) OR @TimeLimit IS NULL)
      BEGIN
        SET @CurrentCommand = 'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; SELECT schemas.[schema_id] AS SchemaID, schemas.[name] AS SchemaName, objects.[object_id] AS ObjectID, objects.[name] AS ObjectName, RTRIM(objects.[type]) AS ObjectType, 0 AS [Order], 0 AS Selected, 0 AS Completed FROM sys.objects objects INNER JOIN sys.schemas schemas ON objects.schema_id = schemas.schema_id LEFT OUTER JOIN sys.tables tables ON objects.object_id = tables.object_id WHERE objects.[type] IN(''U'',''V'') AND EXISTS(SELECT * FROM sys.indexes indexes WHERE indexes.object_id = objects.object_id)' + CASE WHEN @Version >= 12 THEN ' AND (tables.is_memory_optimized = 0 OR is_memory_optimized IS NULL)' ELSE '' END + ' ORDER BY schemas.name ASC, objects.name ASC'

        INSERT INTO @tmpObjects (SchemaID, SchemaName, ObjectID, ObjectName, ObjectType, [Order], Selected, Completed)
        EXECUTE @CurrentDatabase_sp_executesql @stmt = @CurrentCommand
        SET @Error = @@ERROR
        IF @Error <> 0 SET @ReturnCode = @Error

        IF @Objects IS NULL
        BEGIN
          UPDATE tmpObjects
          SET tmpObjects.Selected = 1
          FROM @tmpObjects tmpObjects
        END
        ELSE
        BEGIN
          UPDATE tmpObjects
          SET tmpObjects.Selected = SelectedObjects.Selected
          FROM @tmpObjects tmpObjects
          INNER JOIN @SelectedObjects SelectedObjects
          ON @CurrentDatabaseName LIKE REPLACE(SelectedObjects.DatabaseName,'_','[_]') AND tmpObjects.SchemaName LIKE REPLACE(SelectedObjects.SchemaName,'_','[_]') AND tmpObjects.ObjectName LIKE REPLACE(SelectedObjects.ObjectName,'_','[_]')
          WHERE SelectedObjects.Selected = 1

          UPDATE tmpObjects
          SET tmpObjects.Selected = SelectedObjects.Selected
          FROM @tmpObjects tmpObjects
          INNER JOIN @SelectedObjects SelectedObjects
          ON @CurrentDatabaseName LIKE REPLACE(SelectedObjects.DatabaseName,'_','[_]') AND tmpObjects.SchemaName LIKE REPLACE(SelectedObjects.SchemaName,'_','[_]') AND tmpObjects.ObjectName LIKE REPLACE(SelectedObjects.ObjectName,'_','[_]')
          WHERE SelectedObjects.Selected = 0

          UPDATE tmpObjects
          SET tmpObjects.StartPosition = SelectedObjects2.StartPosition
          FROM @tmpObjects tmpObjects
          INNER JOIN (SELECT tmpObjects.SchemaName, tmpObjects.ObjectName, MIN(SelectedObjects.StartPosition) AS StartPosition
                      FROM @tmpObjects tmpObjects
                      INNER JOIN @SelectedObjects SelectedObjects
                      ON @CurrentDatabaseName LIKE REPLACE(SelectedObjects.DatabaseName,'_','[_]') AND tmpObjects.SchemaName LIKE REPLACE(SelectedObjects.SchemaName,'_','[_]') AND tmpObjects.ObjectName LIKE REPLACE(SelectedObjects.ObjectName,'_','[_]')
                      WHERE SelectedObjects.Selected = 1
                      GROUP BY tmpObjects.SchemaName, tmpObjects.ObjectName) SelectedObjects2
          ON tmpObjects.SchemaName = SelectedObjects2.SchemaName AND tmpObjects.ObjectName = SelectedObjects2.ObjectName
        END;

        WITH tmpObjects AS (
        SELECT SchemaName, ObjectName, [Order], ROW_NUMBER() OVER (ORDER BY StartPosition ASC, SchemaName ASC, ObjectName ASC) AS RowNumber
        FROM @tmpObjects tmpObjects
        WHERE Selected = 1
        )
        UPDATE tmpObjects
        SET [Order] = RowNumber

        SET @ErrorMessage = ''
        SELECT @ErrorMessage = @ErrorMessage + QUOTENAME(DatabaseName) + '.' + QUOTENAME(SchemaName) + '.' + QUOTENAME(ObjectName) + ', '
        FROM @SelectedObjects SelectedObjects
        WHERE DatabaseName = @CurrentDatabaseName
        AND SchemaName NOT LIKE '%[%]%'
        AND ObjectName NOT LIKE '%[%]%'
        AND NOT EXISTS (SELECT * FROM @tmpObjects WHERE SchemaName = SelectedObjects.SchemaName AND ObjectName = SelectedObjects.ObjectName)
        IF @@ROWCOUNT > 0
        BEGIN
          SET @ErrorMessage = 'The following objects do not exist: ' + LEFT(@ErrorMessage,LEN(@ErrorMessage)-1) + '.'
          RAISERROR('%s',10,1,@ErrorMessage) WITH NOWAIT
          SET @Error = @@ERROR
          RAISERROR(@EmptyLine,10,1) WITH NOWAIT
        END

        WHILE (SYSDATETIME() < DATEADD(SECOND,@TimeLimit,@StartTime) OR @TimeLimit IS NULL)
        BEGIN
          SELECT TOP 1 @CurrentOID = ID,
                       @CurrentSchemaID = SchemaID,
                       @CurrentSchemaName = SchemaName,
                       @CurrentObjectID = ObjectID,
                       @CurrentObjectName = ObjectName,
                       @CurrentObjectType = ObjectType
          FROM @tmpObjects
          WHERE Selected = 1
          AND Completed = 0
          ORDER BY [Order] ASC

          IF @@ROWCOUNT = 0
          BEGIN
            BREAK
          END

          -- Does the object exist?
          SET @CurrentCommand = ''
          IF @LockTimeout IS NOT NULL SET @CurrentCommand = 'SET LOCK_TIMEOUT ' + CAST(@LockTimeout * 1000 AS nvarchar) + '; '
          SET @CurrentCommand += 'IF EXISTS(SELECT * FROM sys.objects objects INNER JOIN sys.schemas schemas ON objects.schema_id = schemas.schema_id LEFT OUTER JOIN sys.tables tables ON objects.object_id = tables.object_id WHERE objects.[type] IN(''U'',''V'') AND EXISTS(SELECT * FROM sys.indexes indexes WHERE indexes.object_id = objects.object_id)' + CASE WHEN @Version >= 12 THEN ' AND (tables.is_memory_optimized = 0 OR is_memory_optimized IS NULL)' ELSE '' END + ' AND schemas.[schema_id] = @ParamSchemaID AND schemas.[name] = @ParamSchemaName AND objects.[object_id] = @ParamObjectID AND objects.[name] = @ParamObjectName AND objects.[type] = @ParamObjectType) BEGIN SET @ParamObjectExists = 1 END'

          BEGIN TRY
            EXECUTE @CurrentDatabase_sp_executesql @stmt = @CurrentCommand, @params = N'@ParamSchemaID int, @ParamSchemaName sysname, @ParamObjectID int, @ParamObjectName sysname, @ParamObjectType sysname, @ParamObjectExists bit OUTPUT', @ParamSchemaID = @CurrentSchemaID, @ParamSchemaName = @CurrentSchemaName, @ParamObjectID = @CurrentObjectID, @ParamObjectName = @CurrentObjectName, @ParamObjectType = @CurrentObjectType, @ParamObjectExists = @CurrentObjectExists OUTPUT

            IF @CurrentObjectExists IS NULL SET @CurrentObjectExists = 0
          END TRY
          BEGIN CATCH
            SET @ErrorMessage = 'Msg ' + CAST(ERROR_NUMBER() AS nvarchar) + ', ' + ISNULL(ERROR_MESSAGE(),'') + CASE WHEN ERROR_NUMBER() = 1222 THEN ', ' + 'The object ' + QUOTENAME(@CurrentDatabaseName) + '.' + QUOTENAME(@CurrentSchemaName) + '.' + QUOTENAME(@CurrentObjectName) + ' is locked. It could not be checked if the object exists.' ELSE '' END
            SET @Severity = CASE WHEN ERROR_NUMBER() IN(1205,1222) THEN @LockMessageSeverity ELSE 16 END
            RAISERROR('%s',@Severity,1,@ErrorMessage) WITH NOWAIT
            RAISERROR(@EmptyLine,10,1) WITH NOWAIT

            IF NOT (ERROR_NUMBER() IN(1205,1222) AND @LockMessageSeverity = 10)
            BEGIN
              SET @ReturnCode = ERROR_NUMBER()
            END
          END CATCH

          IF @CurrentObjectExists = 1
          BEGIN
            SET @CurrentDatabaseContext = @CurrentDatabaseName

            SET @CurrentCommandType = 'DBCC_CHECKTABLE'

            SET @CurrentCommand = ''
            IF @LockTimeout IS NOT NULL SET @CurrentCommand = 'SET LOCK_TIMEOUT ' + CAST(@LockTimeout * 1000 AS nvarchar) + '; '
            SET @CurrentCommand += 'DBCC CHECKTABLE (' + QUOTENAME(QUOTENAME(@CurrentSchemaName) + '.' + QUOTENAME(@CurrentObjectName),'''')
            IF @NoIndex = 'Y' SET @CurrentCommand += ', NOINDEX'
            SET @CurrentCommand += ') WITH ALL_ERRORMSGS'
            IF @DataPurity = 'Y' SET @CurrentCommand += ', DATA_PURITY'
            IF @PhysicalOnly = 'Y' SET @CurrentCommand += ', PHYSICAL_ONLY'
            IF @ExtendedLogicalChecks = 'Y' SET @CurrentCommand += ', EXTENDED_LOGICAL_CHECKS'
            IF @NoInformationalMessages = 'Y' SET @CurrentCommand += ', NO_INFOMSGS'
            IF @TabLock = 'Y' SET @CurrentCommand += ', TABLOCK'
            IF @MaxDOP IS NOT NULL SET @CurrentCommand += ', MAXDOP = ' + CAST(@MaxDOP AS nvarchar)

            EXECUTE @CurrentCommandOutput = dbo.CommandExecute @DatabaseContext = @CurrentDatabaseContext, @Command = @CurrentCommand, @CommandType = @CurrentCommandType, @Mode = 1, @DatabaseName = @CurrentDatabaseName, @SchemaName = @CurrentSchemaName, @ObjectName = @CurrentObjectName, @ObjectType = @CurrentObjectType, @LogToTable = @LogToTable, @Execute = @Execute
            SET @Error = @@ERROR
            IF @Error <> 0 SET @CurrentCommandOutput = @Error
            IF @CurrentCommandOutput <> 0 SET @ReturnCode = @CurrentCommandOutput
          END

          UPDATE @tmpObjects
          SET Completed = 1
          WHERE Selected = 1
          AND Completed = 0
          AND ID = @CurrentOID

          SET @CurrentOID = NULL
          SET @CurrentSchemaID = NULL
          SET @CurrentSchemaName = NULL
          SET @CurrentObjectID = NULL
          SET @CurrentObjectName = NULL
          SET @CurrentObjectType = NULL
          SET @CurrentObjectExists = NULL

          SET @CurrentDatabaseContext = NULL
          SET @CurrentCommand = NULL
          SET @CurrentCommandOutput = NULL
          SET @CurrentCommandType = NULL
        END
      END

      -- Check catalog
      IF EXISTS(SELECT * FROM @SelectedCheckCommands WHERE CheckCommand = 'CHECKCATALOG') AND (SYSDATETIME() < DATEADD(SECOND,@TimeLimit,@StartTime) OR @TimeLimit IS NULL)
      BEGIN
        SET @CurrentDatabaseContext = CASE WHEN SERVERPROPERTY('EngineEdition') = 5 THEN @CurrentDatabaseName ELSE 'master' END

        SET @CurrentCommandType = 'DBCC_CHECKCATALOG'

        SET @CurrentCommand = ''
        IF @LockTimeout IS NOT NULL SET @CurrentCommand = 'SET LOCK_TIMEOUT ' + CAST(@LockTimeout * 1000 AS nvarchar) + '; '
        SET @CurrentCommand += 'DBCC CHECKCATALOG (' + QUOTENAME(@CurrentDatabaseName)
        SET @CurrentCommand += ')'
        IF @NoInformationalMessages = 'Y' SET @CurrentCommand += ' WITH NO_INFOMSGS'

        EXECUTE @CurrentCommandOutput = dbo.CommandExecute @DatabaseContext = @CurrentDatabaseContext, @Command = @CurrentCommand, @CommandType = @CurrentCommandType, @Mode = 1, @DatabaseName = @CurrentDatabaseName, @LogToTable = @LogToTable, @Execute = @Execute
        SET @Error = @@ERROR
        IF @Error <> 0 SET @CurrentCommandOutput = @Error
        IF @CurrentCommandOutput <> 0 SET @ReturnCode = @CurrentCommandOutput
      END

    END

    IF @CurrentDatabaseState = 'SUSPECT'
    BEGIN
      SET @ErrorMessage = 'The database ' + QUOTENAME(@CurrentDatabaseName) + ' is in a SUSPECT state.'
      RAISERROR('%s',16,1,@ErrorMessage) WITH NOWAIT
      SET @Error = @@ERROR
      RAISERROR(@EmptyLine,10,1) WITH NOWAIT
    END

    -- Update that the database is completed
    IF @DatabasesInParallel = 'Y'
    BEGIN
      UPDATE dbo.QueueDatabase
      SET DatabaseEndTime = SYSDATETIME()
      WHERE QueueID = @QueueID
      AND DatabaseName = @CurrentDatabaseName
    END
    ELSE
    BEGIN
      UPDATE @tmpDatabases
      SET Completed = 1
      WHERE Selected = 1
      AND Completed = 0
      AND ID = @CurrentDBID
    END

    -- Clear variables
    SET @CurrentDBID = NULL
    SET @CurrentDatabaseName = NULL

    SET @CurrentDatabase_sp_executesql = NULL

    SET @CurrentUserAccess = NULL
    SET @CurrentIsReadOnly = NULL
    SET @CurrentDatabaseState = NULL
    SET @CurrentInStandby = NULL
    SET @CurrentRecoveryModel = NULL

    SET @CurrentReplicaID = NULL
    SET @CurrentAvailabilityGroupID = NULL
    SET @CurrentAvailabilityGroup = NULL
    SET @CurrentAvailabilityGroupRole = NULL
    SET @CurrentAvailabilityGroupBackupPreference = NULL
    SET @CurrentSecondaryRoleAllowConnections = NULL
    SET @CurrentIsPreferredBackupReplica = NULL
    SET @CurrentDatabaseMirroringRole = NULL

    SET @CurrentDatabaseContext = NULL
    SET @CurrentCommand = NULL
    SET @CurrentCommandOutput = NULL
    SET @CurrentCommandType = NULL

    DELETE FROM @tmpFileGroups
    DELETE FROM @tmpObjects

  END

  ----------------------------------------------------------------------------------------------------
  --// Log completing information                                                                 //--
  ----------------------------------------------------------------------------------------------------

  Logging:
  SET @EndMessage = 'Date and time: ' + CONVERT(nvarchar,SYSDATETIME(),120)
  RAISERROR('%s',10,1,@EndMessage) WITH NOWAIT

  RAISERROR(@EmptyLine,10,1) WITH NOWAIT

  IF @ReturnCode <> 0
  BEGIN
    RETURN @ReturnCode
  END

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

END


GO

-- [dbo].[IndexOptimize]

CREATE PROCEDURE [dbo].[IndexOptimize]

@Databases nvarchar(max) = NULL,
@FragmentationLow nvarchar(max) = NULL,
@FragmentationMedium nvarchar(max) = 'INDEX_REORGANIZE,INDEX_REBUILD_ONLINE,INDEX_REBUILD_OFFLINE',
@FragmentationHigh nvarchar(max) = 'INDEX_REBUILD_ONLINE,INDEX_REBUILD_OFFLINE',
@FragmentationLevel1 int = 5,
@FragmentationLevel2 int = 30,
@MinNumberOfPages int = 1000,
@MaxNumberOfPages int = NULL,
@SortInTempdb nvarchar(max) = 'N',
@MaxDOP int = NULL,
@FillFactor int = NULL,
@PadIndex nvarchar(max) = NULL,
@LOBCompaction nvarchar(max) = 'Y',
@UpdateStatistics nvarchar(max) = NULL,
@OnlyModifiedStatistics nvarchar(max) = 'N',
@StatisticsModificationLevel int = NULL,
@StatisticsSample int = NULL,
@StatisticsResample nvarchar(max) = 'N',
@PartitionLevel nvarchar(max) = 'Y',
@MSShippedObjects nvarchar(max) = 'N',
@Indexes nvarchar(max) = NULL,
@TimeLimit int = NULL,
@Delay int = NULL,
@WaitAtLowPriorityMaxDuration int = NULL,
@WaitAtLowPriorityAbortAfterWait nvarchar(max) = NULL,
@Resumable nvarchar(max) = 'N',
@AvailabilityGroups nvarchar(max) = NULL,
@LockTimeout int = NULL,
@LockMessageSeverity int = 16,
@StringDelimiter nvarchar(max) = ',',
@DatabaseOrder nvarchar(max) = NULL,
@DatabasesInParallel nvarchar(max) = 'N',
@ExecuteAsUser nvarchar(max) = NULL,
@LogToTable nvarchar(max) = 'N',
@Execute nvarchar(max) = 'Y'

AS

BEGIN

  ----------------------------------------------------------------------------------------------------
  --// Source:  https://ola.hallengren.com                                                        //--
  --// License: https://ola.hallengren.com/license.html                                           //--
  --// GitHub:  https://github.com/olahallengren/sql-server-maintenance-solution                  //--
  --// Version: 2025-08-23 17:25:24                                                               //--
  ----------------------------------------------------------------------------------------------------

  SET NOCOUNT ON

  SET ARITHABORT ON

  SET NUMERIC_ROUNDABORT OFF

  DECLARE @StartMessage nvarchar(max)
  DECLARE @EndMessage nvarchar(max)
  DECLARE @DatabaseMessage nvarchar(max)
  DECLARE @ErrorMessage nvarchar(max)
  DECLARE @Severity int

  DECLARE @StartTime datetime2 = SYSDATETIME()
  DECLARE @SchemaName nvarchar(max) = OBJECT_SCHEMA_NAME(@@PROCID)
  DECLARE @ObjectName nvarchar(max) = OBJECT_NAME(@@PROCID)
  DECLARE @VersionTimestamp nvarchar(max) = SUBSTRING(OBJECT_DEFINITION(@@PROCID),CHARINDEX('--// Version: ',OBJECT_DEFINITION(@@PROCID)) + LEN('--// Version: ') + 1, 19)
  DECLARE @Parameters nvarchar(max)

  DECLARE @HostPlatform nvarchar(max)

  DECLARE @PartitionLevelStatistics bit

  DECLARE @QueueID int
  DECLARE @QueueStartTime datetime2

  DECLARE @CurrentDBID int
  DECLARE @CurrentDatabaseName nvarchar(max)

  DECLARE @CurrentDatabase_sp_executesql nvarchar(max)

  DECLARE @CurrentExecuteAsUserExists bit
  DECLARE @CurrentUserAccess nvarchar(max)
  DECLARE @CurrentIsReadOnly bit
  DECLARE @CurrentDatabaseState nvarchar(max)
  DECLARE @CurrentInStandby bit
  DECLARE @CurrentRecoveryModel nvarchar(max)

  DECLARE @CurrentReplicaID uniqueidentifier
  DECLARE @CurrentAvailabilityGroupID uniqueidentifier
  DECLARE @CurrentAvailabilityGroup nvarchar(max)
  DECLARE @CurrentAvailabilityGroupRole nvarchar(max)
  DECLARE @CurrentDatabaseMirroringRole nvarchar(max)

  DECLARE @CurrentDatabaseContext nvarchar(max)
  DECLARE @CurrentCommand nvarchar(max)
  DECLARE @CurrentCommandOutput int
  DECLARE @CurrentCommandType nvarchar(max)
  DECLARE @CurrentComment nvarchar(max)
  DECLARE @CurrentExtendedInfo xml

  DECLARE @Errors TABLE (ID int IDENTITY PRIMARY KEY,
                         [Message] nvarchar(max) NOT NULL,
                         Severity int NOT NULL,
                         [State] int)

  DECLARE @CurrentMessage nvarchar(max)
  DECLARE @CurrentSeverity int
  DECLARE @CurrentState int

  DECLARE @CurrentIxID int
  DECLARE @CurrentIxOrder int
  DECLARE @CurrentSchemaID int
  DECLARE @CurrentSchemaName nvarchar(max)
  DECLARE @CurrentObjectID int
  DECLARE @CurrentObjectName nvarchar(max)
  DECLARE @CurrentObjectType nvarchar(max)
  DECLARE @CurrentIsMemoryOptimized bit
  DECLARE @CurrentIndexID int
  DECLARE @CurrentIndexName nvarchar(max)
  DECLARE @CurrentIndexType int
  DECLARE @CurrentStatisticsID int
  DECLARE @CurrentStatisticsName nvarchar(max)
  DECLARE @CurrentPartitionID bigint
  DECLARE @CurrentPartitionNumber int
  DECLARE @CurrentPartitionCount int
  DECLARE @CurrentIsPartition bit
  DECLARE @CurrentIndexExists bit
  DECLARE @CurrentStatisticsExists bit
  DECLARE @CurrentIsImageText bit
  DECLARE @CurrentIsNewLOB bit
  DECLARE @CurrentIsFileStream bit
  DECLARE @CurrentHasClusteredColumnstore bit
  DECLARE @CurrentHasNonClusteredColumnstore bit
  DECLARE @CurrentIsComputed bit
  DECLARE @CurrentIsClusteredIndexComputed bit
  DECLARE @CurrentIsTimestamp bit
  DECLARE @CurrentAllowPageLocks bit
  DECLARE @CurrentHasFilter bit
  DECLARE @CurrentNoRecompute bit
  DECLARE @CurrentIsIncremental bit
  DECLARE @CurrentRowCount bigint
  DECLARE @CurrentModificationCounter bigint
  DECLARE @CurrentOnReadOnlyFileGroup bit
  DECLARE @CurrentResumableIndexOperation bit
  DECLARE @CurrentFragmentationLevel float
  DECLARE @CurrentPageCount bigint
  DECLARE @CurrentFragmentationGroup nvarchar(max)
  DECLARE @CurrentAction nvarchar(max)
  DECLARE @CurrentMaxDOP int
  DECLARE @CurrentUpdateStatistics nvarchar(max)
  DECLARE @CurrentStatisticsSample int
  DECLARE @CurrentStatisticsResample nvarchar(max)
  DECLARE @CurrentDelay datetime

  DECLARE @tmpDatabases TABLE (ID int IDENTITY,
                               DatabaseName nvarchar(max),
                               DatabaseType nvarchar(max),
                               AvailabilityGroup bit,
                               StartPosition int,
                               DatabaseSize bigint,
                               [Order] int,
                               Selected bit,
                               Completed bit,
                               PRIMARY KEY(Selected, Completed, [Order], ID))

  DECLARE @tmpAvailabilityGroups TABLE (ID int IDENTITY PRIMARY KEY,
                                        AvailabilityGroupName nvarchar(max),
                                        StartPosition int,
                                        Selected bit)

  DECLARE @tmpDatabasesAvailabilityGroups TABLE (DatabaseName nvarchar(max),
                                                 AvailabilityGroupName nvarchar(max))

  DECLARE @tmpIndexesStatistics TABLE (ID int IDENTITY,
                                       SchemaID int,
                                       SchemaName nvarchar(max),
                                       ObjectID int,
                                       ObjectName nvarchar(max),
                                       ObjectType nvarchar(max),
                                       IsMemoryOptimized bit,
                                       IndexID int,
                                       IndexName nvarchar(max),
                                       IndexType int,
                                       AllowPageLocks bit,
                                       HasFilter bit,
                                       IsImageText bit,
                                       IsNewLOB bit,
                                       IsFileStream bit,
                                       HasClusteredColumnstore bit,
                                       HasNonClusteredColumnstore bit,
                                       IsComputed bit,
                                       IsClusteredIndexComputed bit,
                                       IsTimestamp bit,
                                       OnReadOnlyFileGroup bit,
                                       ResumableIndexOperation bit,
                                       StatisticsID int,
                                       StatisticsName nvarchar(max),
                                       [NoRecompute] bit,
                                       IsIncremental bit,
                                       PartitionID bigint,
                                       PartitionNumber int,
                                       PartitionCount int,
                                       StartPosition int,
                                       [Order] int,
                                       Selected bit,
                                       Completed bit,
                                       PRIMARY KEY(Selected, Completed, [Order], ID))

  DECLARE @SelectedDatabases TABLE (DatabaseName nvarchar(max),
                                    DatabaseType nvarchar(max),
                                    AvailabilityGroup nvarchar(max),
                                    StartPosition int,
                                    Selected bit)

  DECLARE @SelectedAvailabilityGroups TABLE (AvailabilityGroupName nvarchar(max),
                                             StartPosition int,
                                             Selected bit)

  DECLARE @SelectedIndexes TABLE (DatabaseName nvarchar(max),
                                  SchemaName nvarchar(max),
                                  ObjectName nvarchar(max),
                                  IndexName nvarchar(max),
                                  StartPosition int,
                                  Selected bit)

  DECLARE @Actions TABLE ([Action] nvarchar(max))

  INSERT INTO @Actions([Action]) VALUES('INDEX_REBUILD_ONLINE')
  INSERT INTO @Actions([Action]) VALUES('INDEX_REBUILD_OFFLINE')
  INSERT INTO @Actions([Action]) VALUES('INDEX_REORGANIZE')

  DECLARE @ActionsPreferred TABLE (FragmentationGroup nvarchar(max),
                                   [Priority] int,
                                   [Action] nvarchar(max))

  DECLARE @CurrentActionsAllowed TABLE ([Action] nvarchar(max))

  DECLARE @CurrentAlterIndexWithClauseArguments TABLE (ID int IDENTITY,
                                                       Argument nvarchar(max),
                                                       Added bit DEFAULT 0)

  DECLARE @CurrentAlterIndexArgumentID int
  DECLARE @CurrentAlterIndexArgument nvarchar(max)
  DECLARE @CurrentAlterIndexWithClause nvarchar(max)

  DECLARE @CurrentUpdateStatisticsWithClauseArguments TABLE (ID int IDENTITY,
                                                             Argument nvarchar(max),
                                                             Added bit DEFAULT 0)

  DECLARE @CurrentUpdateStatisticsArgumentID int
  DECLARE @CurrentUpdateStatisticsArgument nvarchar(max)
  DECLARE @CurrentUpdateStatisticsWithClause nvarchar(max)

  DECLARE @Error int = 0
  DECLARE @ReturnCode int = 0

  DECLARE @EmptyLine nvarchar(max) = CHAR(9)

  DECLARE @Version numeric(18,10) = CAST(LEFT(CAST(SERVERPROPERTY('ProductVersion') AS nvarchar(max)),CHARINDEX('.',CAST(SERVERPROPERTY('ProductVersion') AS nvarchar(max))) - 1) + '.' + REPLACE(RIGHT(CAST(SERVERPROPERTY('ProductVersion') AS nvarchar(max)), LEN(CAST(SERVERPROPERTY('ProductVersion') AS nvarchar(max))) - CHARINDEX('.',CAST(SERVERPROPERTY('ProductVersion') AS nvarchar(max)))),'.','') AS numeric(18,10))

  IF @Version >= 14
  BEGIN
    SELECT @HostPlatform = host_platform
    FROM sys.dm_os_host_info
  END
  ELSE
  BEGIN
    SET @HostPlatform = 'Windows'
  END

  DECLARE @AmazonRDS bit = CASE WHEN SERVERPROPERTY('EngineEdition') IN (5, 8) THEN 0 WHEN EXISTS (SELECT * FROM sys.databases WHERE [name] = 'rdsadmin') AND SUSER_SNAME(0x01) = 'rdsa' THEN 1 ELSE 0 END

  ----------------------------------------------------------------------------------------------------
  --// Log initial information                                                                    //--
  ----------------------------------------------------------------------------------------------------

  SET @Parameters = '@Databases = ' + ISNULL('''' + REPLACE(@Databases,'''','''''') + '''','NULL')
  SET @Parameters += ', @FragmentationLow = ' + ISNULL('''' + REPLACE(@FragmentationLow,'''','''''') + '''','NULL')
  SET @Parameters += ', @FragmentationMedium = ' + ISNULL('''' + REPLACE(@FragmentationMedium,'''','''''') + '''','NULL')
  SET @Parameters += ', @FragmentationHigh = ' + ISNULL('''' + REPLACE(@FragmentationHigh,'''','''''') + '''','NULL')
  SET @Parameters += ', @FragmentationLevel1 = ' + ISNULL(CAST(@FragmentationLevel1 AS nvarchar),'NULL')
  SET @Parameters += ', @FragmentationLevel2 = ' + ISNULL(CAST(@FragmentationLevel2 AS nvarchar),'NULL')
  SET @Parameters += ', @MinNumberOfPages = ' + ISNULL(CAST(@MinNumberOfPages AS nvarchar),'NULL')
  SET @Parameters += ', @MaxNumberOfPages = ' + ISNULL(CAST(@MaxNumberOfPages AS nvarchar),'NULL')
  SET @Parameters += ', @SortInTempdb = ' + ISNULL('''' + REPLACE(@SortInTempdb,'''','''''') + '''','NULL')
  SET @Parameters += ', @MaxDOP = ' + ISNULL(CAST(@MaxDOP AS nvarchar),'NULL')
  SET @Parameters += ', @FillFactor = ' + ISNULL(CAST(@FillFactor AS nvarchar),'NULL')
  SET @Parameters += ', @PadIndex = ' + ISNULL('''' + REPLACE(@PadIndex,'''','''''') + '''','NULL')
  SET @Parameters += ', @LOBCompaction = ' + ISNULL('''' + REPLACE(@LOBCompaction,'''','''''') + '''','NULL')
  SET @Parameters += ', @UpdateStatistics = ' + ISNULL('''' + REPLACE(@UpdateStatistics,'''','''''') + '''','NULL')
  SET @Parameters += ', @OnlyModifiedStatistics = ' + ISNULL('''' + REPLACE(@OnlyModifiedStatistics,'''','''''') + '''','NULL')
  SET @Parameters += ', @StatisticsModificationLevel = ' + ISNULL(CAST(@StatisticsModificationLevel AS nvarchar),'NULL')
  SET @Parameters += ', @StatisticsSample = ' + ISNULL(CAST(@StatisticsSample AS nvarchar),'NULL')
  SET @Parameters += ', @StatisticsResample = ' + ISNULL('''' + REPLACE(@StatisticsResample,'''','''''') + '''','NULL')
  SET @Parameters += ', @PartitionLevel = ' + ISNULL('''' + REPLACE(@PartitionLevel,'''','''''') + '''','NULL')
  SET @Parameters += ', @MSShippedObjects = ' + ISNULL('''' + REPLACE(@MSShippedObjects,'''','''''') + '''','NULL')
  SET @Parameters += ', @Indexes = ' + ISNULL('''' + REPLACE(@Indexes,'''','''''') + '''','NULL')
  SET @Parameters += ', @TimeLimit = ' + ISNULL(CAST(@TimeLimit AS nvarchar),'NULL')
  SET @Parameters += ', @Delay = ' + ISNULL(CAST(@Delay AS nvarchar),'NULL')
  SET @Parameters += ', @WaitAtLowPriorityMaxDuration = ' + ISNULL(CAST(@WaitAtLowPriorityMaxDuration AS nvarchar),'NULL')
  SET @Parameters += ', @WaitAtLowPriorityAbortAfterWait = ' + ISNULL('''' + REPLACE(@WaitAtLowPriorityAbortAfterWait,'''','''''') + '''','NULL')
  SET @Parameters += ', @Resumable = ' + ISNULL('''' + REPLACE(@Resumable,'''','''''') + '''','NULL')
  SET @Parameters += ', @AvailabilityGroups = ' + ISNULL('''' + REPLACE(@AvailabilityGroups,'''','''''') + '''','NULL')
  SET @Parameters += ', @LockTimeout = ' + ISNULL(CAST(@LockTimeout AS nvarchar),'NULL')
  SET @Parameters += ', @LockMessageSeverity = ' + ISNULL(CAST(@LockMessageSeverity AS nvarchar),'NULL')
  SET @Parameters += ', @StringDelimiter = ' + ISNULL('''' + REPLACE(@StringDelimiter,'''','''''') + '''','NULL')
  SET @Parameters += ', @DatabaseOrder = ' + ISNULL('''' + REPLACE(@DatabaseOrder,'''','''''') + '''','NULL')
  SET @Parameters += ', @DatabasesInParallel = ' + ISNULL('''' + REPLACE(@DatabasesInParallel,'''','''''') + '''','NULL')
  SET @Parameters += ', @ExecuteAsUser = ' + ISNULL('''' + REPLACE(@ExecuteAsUser,'''','''''') + '''','NULL')
  SET @Parameters += ', @LogToTable = ' + ISNULL('''' + REPLACE(@LogToTable,'''','''''') + '''','NULL')
  SET @Parameters += ', @Execute = ' + ISNULL('''' + REPLACE(@Execute,'''','''''') + '''','NULL')

  SET @StartMessage = 'Date and time: ' + CONVERT(nvarchar,@StartTime,120)
  RAISERROR('%s',10,1,@StartMessage) WITH NOWAIT

  SET @StartMessage = 'Server: ' + CAST(SERVERPROPERTY('ServerName') AS nvarchar(max))
  RAISERROR('%s',10,1,@StartMessage) WITH NOWAIT

  SET @StartMessage = 'Version: ' + CAST(SERVERPROPERTY('ProductVersion') AS nvarchar(max))
  RAISERROR('%s',10,1,@StartMessage) WITH NOWAIT

  SET @StartMessage = 'Edition: ' + CAST(SERVERPROPERTY('Edition') AS nvarchar(max))
  RAISERROR('%s',10,1,@StartMessage) WITH NOWAIT

  SET @StartMessage = 'Platform: ' + @HostPlatform
  RAISERROR('%s',10,1,@StartMessage) WITH NOWAIT

  SET @StartMessage = 'Procedure: ' + QUOTENAME(DB_NAME()) + '.' + QUOTENAME(@SchemaName) + '.' + QUOTENAME(@ObjectName)
  RAISERROR('%s',10,1,@StartMessage) WITH NOWAIT

  SET @StartMessage = 'Parameters: ' + @Parameters
  RAISERROR('%s',10,1,@StartMessage) WITH NOWAIT

  SET @StartMessage = 'Version: ' + @VersionTimestamp
  RAISERROR('%s',10,1,@StartMessage) WITH NOWAIT

  SET @StartMessage = 'Source: https://ola.hallengren.com'
  RAISERROR('%s',10,1,@StartMessage) WITH NOWAIT

  RAISERROR(@EmptyLine,10,1) WITH NOWAIT

  ----------------------------------------------------------------------------------------------------
  --// Check core requirements                                                                    //--
  ----------------------------------------------------------------------------------------------------

  IF NOT (SELECT [compatibility_level] FROM sys.databases WHERE [name] = DB_NAME()) >= 90
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The database ' + QUOTENAME(DB_NAME()) + ' has to be in compatibility level 90 or higher.', 16, 1
  END

  IF NOT (SELECT uses_ansi_nulls FROM sys.sql_modules WHERE [object_id] = @@PROCID) = 1
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'ANSI_NULLS has to be set to ON for the stored procedure.', 16, 1
  END

  IF NOT (SELECT uses_quoted_identifier FROM sys.sql_modules WHERE [object_id] = @@PROCID) = 1
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'QUOTED_IDENTIFIER has to be set to ON for the stored procedure.', 16, 1
  END

  IF NOT EXISTS (SELECT * FROM sys.objects objects INNER JOIN sys.schemas schemas ON objects.[schema_id] = schemas.[schema_id] WHERE objects.[type] = 'P' AND schemas.[name] = 'dbo' AND objects.[name] = 'CommandExecute')
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The stored procedure CommandExecute is missing. Download https://ola.hallengren.com/scripts/CommandExecute.sql.', 16, 1
  END

  IF EXISTS (SELECT * FROM sys.objects objects INNER JOIN sys.schemas schemas ON objects.[schema_id] = schemas.[schema_id] WHERE objects.[type] = 'P' AND schemas.[name] = 'dbo' AND objects.[name] = 'CommandExecute' AND OBJECT_DEFINITION(objects.[object_id]) NOT LIKE '%@DatabaseContext%')
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The stored procedure CommandExecute needs to be updated. Download https://ola.hallengren.com/scripts/CommandExecute.sql.', 16, 1
  END

  IF @LogToTable = 'Y' AND NOT EXISTS (SELECT * FROM sys.objects objects INNER JOIN sys.schemas schemas ON objects.[schema_id] = schemas.[schema_id] WHERE objects.[type] = 'U' AND schemas.[name] = 'dbo' AND objects.[name] = 'CommandLog')
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The table CommandLog is missing. Download https://ola.hallengren.com/scripts/CommandLog.sql.', 16, 1
  END

  IF @DatabasesInParallel = 'Y' AND NOT EXISTS (SELECT * FROM sys.objects objects INNER JOIN sys.schemas schemas ON objects.[schema_id] = schemas.[schema_id] WHERE objects.[type] = 'U' AND schemas.[name] = 'dbo' AND objects.[name] = 'Queue')
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The table Queue is missing. Download https://ola.hallengren.com/scripts/Queue.sql.', 16, 1
  END

  IF @DatabasesInParallel = 'Y' AND NOT EXISTS (SELECT * FROM sys.objects objects INNER JOIN sys.schemas schemas ON objects.[schema_id] = schemas.[schema_id] WHERE objects.[type] = 'U' AND schemas.[name] = 'dbo' AND objects.[name] = 'QueueDatabase')
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The table QueueDatabase is missing. Download https://ola.hallengren.com/scripts/QueueDatabase.sql.', 16, 1
  END

  IF @@TRANCOUNT <> 0
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The transaction count is not 0.', 16, 1
  END

  ----------------------------------------------------------------------------------------------------
  --// Select databases                                                                           //--
  ----------------------------------------------------------------------------------------------------

  SET @Databases = REPLACE(@Databases, CHAR(10), '')
  SET @Databases = REPLACE(@Databases, CHAR(13), '')

  WHILE CHARINDEX(@StringDelimiter + ' ', @Databases) > 0 SET @Databases = REPLACE(@Databases, @StringDelimiter + ' ', @StringDelimiter)
  WHILE CHARINDEX(' ' + @StringDelimiter, @Databases) > 0 SET @Databases = REPLACE(@Databases, ' ' + @StringDelimiter, @StringDelimiter)

  SET @Databases = LTRIM(RTRIM(@Databases));

  WITH Databases1 (StartPosition, EndPosition, DatabaseItem) AS
  (
  SELECT 1 AS StartPosition,
         ISNULL(NULLIF(CHARINDEX(@StringDelimiter, @Databases, 1), 0), LEN(@Databases) + 1) AS EndPosition,
         SUBSTRING(@Databases, 1, ISNULL(NULLIF(CHARINDEX(@StringDelimiter, @Databases, 1), 0), LEN(@Databases) + 1) - 1) AS DatabaseItem
  WHERE @Databases IS NOT NULL
  UNION ALL
  SELECT CAST(EndPosition AS int) + 1 AS StartPosition,
         ISNULL(NULLIF(CHARINDEX(@StringDelimiter, @Databases, EndPosition + 1), 0), LEN(@Databases) + 1) AS EndPosition,
         SUBSTRING(@Databases, EndPosition + 1, ISNULL(NULLIF(CHARINDEX(@StringDelimiter, @Databases, EndPosition + 1), 0), LEN(@Databases) + 1) - EndPosition - 1) AS DatabaseItem
  FROM Databases1
  WHERE EndPosition < LEN(@Databases) + 1
  ),
  Databases2 (DatabaseItem, StartPosition, Selected) AS
  (
  SELECT CASE WHEN DatabaseItem LIKE '-%' THEN RIGHT(DatabaseItem,LEN(DatabaseItem) - 1) ELSE DatabaseItem END AS DatabaseItem,
         StartPosition,
         CASE WHEN DatabaseItem LIKE '-%' THEN 0 ELSE 1 END AS Selected
  FROM Databases1
  ),
  Databases3 (DatabaseItem, DatabaseType, AvailabilityGroup, StartPosition, Selected) AS
  (
  SELECT CASE WHEN DatabaseItem IN('ALL_DATABASES','SYSTEM_DATABASES','USER_DATABASES','AVAILABILITY_GROUP_DATABASES') THEN '%' ELSE DatabaseItem END AS DatabaseItem,
         CASE WHEN DatabaseItem = 'SYSTEM_DATABASES' THEN 'S' WHEN DatabaseItem = 'USER_DATABASES' THEN 'U' ELSE NULL END AS DatabaseType,
         CASE WHEN DatabaseItem = 'AVAILABILITY_GROUP_DATABASES' THEN 1 ELSE NULL END AvailabilityGroup,
         StartPosition,
         Selected
  FROM Databases2
  ),
  Databases4 (DatabaseName, DatabaseType, AvailabilityGroup, StartPosition, Selected) AS
  (
  SELECT CASE WHEN LEFT(DatabaseItem,1) = '[' AND RIGHT(DatabaseItem,1) = ']' THEN PARSENAME(DatabaseItem,1) ELSE DatabaseItem END AS DatabaseItem,
         DatabaseType,
         AvailabilityGroup,
         StartPosition,
         Selected
  FROM Databases3
  )
  INSERT INTO @SelectedDatabases (DatabaseName, DatabaseType, AvailabilityGroup, StartPosition, Selected)
  SELECT DatabaseName,
         DatabaseType,
         AvailabilityGroup,
         StartPosition,
         Selected
  FROM Databases4
  OPTION (MAXRECURSION 0)

  IF @Version >= 11 AND SERVERPROPERTY('IsHadrEnabled') = 1
  BEGIN
    INSERT INTO @tmpAvailabilityGroups (AvailabilityGroupName, Selected)
    SELECT name AS AvailabilityGroupName,
           0 AS Selected
    FROM sys.availability_groups

    INSERT INTO @tmpDatabasesAvailabilityGroups (DatabaseName, AvailabilityGroupName)
    SELECT databases.name,
           availability_groups.name
    FROM sys.databases databases
    INNER JOIN sys.availability_replicas availability_replicas ON databases.replica_id = availability_replicas.replica_id
    INNER JOIN sys.availability_groups availability_groups ON availability_replicas.group_id = availability_groups.group_id
  END

  INSERT INTO @tmpDatabases (DatabaseName, DatabaseType, AvailabilityGroup, [Order], Selected, Completed)
  SELECT [name] AS DatabaseName,
         CASE WHEN name IN('master','msdb','model') OR is_distributor = 1 THEN 'S' ELSE 'U' END AS DatabaseType,
         NULL AS AvailabilityGroup,
         0 AS [Order],
         0 AS Selected,
         0 AS Completed
  FROM sys.databases
  WHERE [name] <> 'tempdb'
  AND source_database_id IS NULL
  ORDER BY [name] ASC

  UPDATE tmpDatabases
  SET AvailabilityGroup = CASE WHEN EXISTS (SELECT * FROM @tmpDatabasesAvailabilityGroups WHERE DatabaseName = tmpDatabases.DatabaseName) THEN 1 ELSE 0 END
  FROM @tmpDatabases tmpDatabases

  UPDATE tmpDatabases
  SET tmpDatabases.Selected = SelectedDatabases.Selected
  FROM @tmpDatabases tmpDatabases
  INNER JOIN @SelectedDatabases SelectedDatabases
  ON tmpDatabases.DatabaseName LIKE REPLACE(SelectedDatabases.DatabaseName,'_','[_]')
  AND (tmpDatabases.DatabaseType = SelectedDatabases.DatabaseType OR SelectedDatabases.DatabaseType IS NULL)
  AND (tmpDatabases.AvailabilityGroup = SelectedDatabases.AvailabilityGroup OR SelectedDatabases.AvailabilityGroup IS NULL)
  WHERE SelectedDatabases.Selected = 1

  UPDATE tmpDatabases
  SET tmpDatabases.Selected = SelectedDatabases.Selected
  FROM @tmpDatabases tmpDatabases
  INNER JOIN @SelectedDatabases SelectedDatabases
  ON tmpDatabases.DatabaseName LIKE REPLACE(SelectedDatabases.DatabaseName,'_','[_]')
  AND (tmpDatabases.DatabaseType = SelectedDatabases.DatabaseType OR SelectedDatabases.DatabaseType IS NULL)
  AND (tmpDatabases.AvailabilityGroup = SelectedDatabases.AvailabilityGroup OR SelectedDatabases.AvailabilityGroup IS NULL)
  WHERE SelectedDatabases.Selected = 0

  UPDATE tmpDatabases
  SET tmpDatabases.StartPosition = SelectedDatabases2.StartPosition
  FROM @tmpDatabases tmpDatabases
  INNER JOIN (SELECT tmpDatabases.DatabaseName, MIN(SelectedDatabases.StartPosition) AS StartPosition
              FROM @tmpDatabases tmpDatabases
              INNER JOIN @SelectedDatabases SelectedDatabases
              ON tmpDatabases.DatabaseName LIKE REPLACE(SelectedDatabases.DatabaseName,'_','[_]')
              AND (tmpDatabases.DatabaseType = SelectedDatabases.DatabaseType OR SelectedDatabases.DatabaseType IS NULL)
              AND (tmpDatabases.AvailabilityGroup = SelectedDatabases.AvailabilityGroup OR SelectedDatabases.AvailabilityGroup IS NULL)
              WHERE SelectedDatabases.Selected = 1
              GROUP BY tmpDatabases.DatabaseName) SelectedDatabases2
  ON tmpDatabases.DatabaseName = SelectedDatabases2.DatabaseName

  IF @Databases IS NOT NULL AND (NOT EXISTS(SELECT * FROM @SelectedDatabases) OR EXISTS(SELECT * FROM @SelectedDatabases WHERE DatabaseName IS NULL OR DATALENGTH(DatabaseName) = 0))
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @Databases is not supported.', 16, 1
  END

  ----------------------------------------------------------------------------------------------------
  --// Select availability groups                                                                 //--
  ----------------------------------------------------------------------------------------------------

  IF @AvailabilityGroups IS NOT NULL AND @Version >= 11 AND SERVERPROPERTY('IsHadrEnabled') = 1
  BEGIN

    SET @AvailabilityGroups = REPLACE(@AvailabilityGroups, CHAR(10), '')
    SET @AvailabilityGroups = REPLACE(@AvailabilityGroups, CHAR(13), '')

    WHILE CHARINDEX(@StringDelimiter + ' ', @AvailabilityGroups) > 0 SET @AvailabilityGroups = REPLACE(@AvailabilityGroups, @StringDelimiter + ' ', @StringDelimiter)
    WHILE CHARINDEX(' ' + @StringDelimiter, @AvailabilityGroups) > 0 SET @AvailabilityGroups = REPLACE(@AvailabilityGroups, ' ' + @StringDelimiter, @StringDelimiter)

    SET @AvailabilityGroups = LTRIM(RTRIM(@AvailabilityGroups));

    WITH AvailabilityGroups1 (StartPosition, EndPosition, AvailabilityGroupItem) AS
    (
    SELECT 1 AS StartPosition,
           ISNULL(NULLIF(CHARINDEX(@StringDelimiter, @AvailabilityGroups, 1), 0), LEN(@AvailabilityGroups) + 1) AS EndPosition,
           SUBSTRING(@AvailabilityGroups, 1, ISNULL(NULLIF(CHARINDEX(@StringDelimiter, @AvailabilityGroups, 1), 0), LEN(@AvailabilityGroups) + 1) - 1) AS AvailabilityGroupItem
    WHERE @AvailabilityGroups IS NOT NULL
    UNION ALL
    SELECT CAST(EndPosition AS int) + 1 AS StartPosition,
           ISNULL(NULLIF(CHARINDEX(@StringDelimiter, @AvailabilityGroups, EndPosition + 1), 0), LEN(@AvailabilityGroups) + 1) AS EndPosition,
           SUBSTRING(@AvailabilityGroups, EndPosition + 1, ISNULL(NULLIF(CHARINDEX(@StringDelimiter, @AvailabilityGroups, EndPosition + 1), 0), LEN(@AvailabilityGroups) + 1) - EndPosition - 1) AS AvailabilityGroupItem
    FROM AvailabilityGroups1
    WHERE EndPosition < LEN(@AvailabilityGroups) + 1
    ),
    AvailabilityGroups2 (AvailabilityGroupItem, StartPosition, Selected) AS
    (
    SELECT CASE WHEN AvailabilityGroupItem LIKE '-%' THEN RIGHT(AvailabilityGroupItem,LEN(AvailabilityGroupItem) - 1) ELSE AvailabilityGroupItem END AS AvailabilityGroupItem,
           StartPosition,
           CASE WHEN AvailabilityGroupItem LIKE '-%' THEN 0 ELSE 1 END AS Selected
    FROM AvailabilityGroups1
    ),
    AvailabilityGroups3 (AvailabilityGroupItem, StartPosition, Selected) AS
    (
    SELECT CASE WHEN AvailabilityGroupItem = 'ALL_AVAILABILITY_GROUPS' THEN '%' ELSE AvailabilityGroupItem END AS AvailabilityGroupItem,
           StartPosition,
           Selected
    FROM AvailabilityGroups2
    ),
    AvailabilityGroups4 (AvailabilityGroupName, StartPosition, Selected) AS
    (
    SELECT CASE WHEN LEFT(AvailabilityGroupItem,1) = '[' AND RIGHT(AvailabilityGroupItem,1) = ']' THEN PARSENAME(AvailabilityGroupItem,1) ELSE AvailabilityGroupItem END AS AvailabilityGroupItem,
           StartPosition,
           Selected
    FROM AvailabilityGroups3
    )
    INSERT INTO @SelectedAvailabilityGroups (AvailabilityGroupName, StartPosition, Selected)
    SELECT AvailabilityGroupName, StartPosition, Selected
    FROM AvailabilityGroups4
    OPTION (MAXRECURSION 0)

    UPDATE tmpAvailabilityGroups
    SET tmpAvailabilityGroups.Selected = SelectedAvailabilityGroups.Selected
    FROM @tmpAvailabilityGroups tmpAvailabilityGroups
    INNER JOIN @SelectedAvailabilityGroups SelectedAvailabilityGroups
    ON tmpAvailabilityGroups.AvailabilityGroupName LIKE REPLACE(SelectedAvailabilityGroups.AvailabilityGroupName,'_','[_]')
    WHERE SelectedAvailabilityGroups.Selected = 1

    UPDATE tmpAvailabilityGroups
    SET tmpAvailabilityGroups.Selected = SelectedAvailabilityGroups.Selected
    FROM @tmpAvailabilityGroups tmpAvailabilityGroups
    INNER JOIN @SelectedAvailabilityGroups SelectedAvailabilityGroups
    ON tmpAvailabilityGroups.AvailabilityGroupName LIKE REPLACE(SelectedAvailabilityGroups.AvailabilityGroupName,'_','[_]')
    WHERE SelectedAvailabilityGroups.Selected = 0

    UPDATE tmpAvailabilityGroups
    SET tmpAvailabilityGroups.StartPosition = SelectedAvailabilityGroups2.StartPosition
    FROM @tmpAvailabilityGroups tmpAvailabilityGroups
    INNER JOIN (SELECT tmpAvailabilityGroups.AvailabilityGroupName, MIN(SelectedAvailabilityGroups.StartPosition) AS StartPosition
                FROM @tmpAvailabilityGroups tmpAvailabilityGroups
                INNER JOIN @SelectedAvailabilityGroups SelectedAvailabilityGroups
                ON tmpAvailabilityGroups.AvailabilityGroupName LIKE REPLACE(SelectedAvailabilityGroups.AvailabilityGroupName,'_','[_]')
                WHERE SelectedAvailabilityGroups.Selected = 1
                GROUP BY tmpAvailabilityGroups.AvailabilityGroupName) SelectedAvailabilityGroups2
    ON tmpAvailabilityGroups.AvailabilityGroupName = SelectedAvailabilityGroups2.AvailabilityGroupName

    UPDATE tmpDatabases
    SET tmpDatabases.StartPosition = tmpAvailabilityGroups.StartPosition,
        tmpDatabases.Selected = 1
    FROM @tmpDatabases tmpDatabases
    INNER JOIN @tmpDatabasesAvailabilityGroups tmpDatabasesAvailabilityGroups ON tmpDatabases.DatabaseName = tmpDatabasesAvailabilityGroups.DatabaseName
    INNER JOIN @tmpAvailabilityGroups tmpAvailabilityGroups ON tmpDatabasesAvailabilityGroups.AvailabilityGroupName = tmpAvailabilityGroups.AvailabilityGroupName
    WHERE tmpAvailabilityGroups.Selected = 1

  END

  IF @AvailabilityGroups IS NOT NULL AND (NOT EXISTS(SELECT * FROM @SelectedAvailabilityGroups) OR EXISTS(SELECT * FROM @SelectedAvailabilityGroups WHERE AvailabilityGroupName IS NULL OR AvailabilityGroupName = '') OR @Version < 11 OR SERVERPROPERTY('IsHadrEnabled') = 0)
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @AvailabilityGroups is not supported.', 16, 1
  END

  IF (@Databases IS NULL AND @AvailabilityGroups IS NULL)
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'You need to specify one of the parameters @Databases and @AvailabilityGroups.', 16, 2
  END

  IF (@Databases IS NOT NULL AND @AvailabilityGroups IS NOT NULL)
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'You can only specify one of the parameters @Databases and @AvailabilityGroups.', 16, 3
  END

  ----------------------------------------------------------------------------------------------------
  --// Select indexes                                                                             //--
  ----------------------------------------------------------------------------------------------------

  SET @Indexes = REPLACE(@Indexes, CHAR(10), '')
  SET @Indexes = REPLACE(@Indexes, CHAR(13), '')

  WHILE CHARINDEX(@StringDelimiter + ' ', @Indexes) > 0 SET @Indexes = REPLACE(@Indexes, @StringDelimiter + ' ', @StringDelimiter)
  WHILE CHARINDEX(' ' + @StringDelimiter, @Indexes) > 0 SET @Indexes = REPLACE(@Indexes, ' ' + @StringDelimiter, @StringDelimiter)

  SET @Indexes = LTRIM(RTRIM(@Indexes));

  WITH Indexes1 (StartPosition, EndPosition, IndexItem) AS
  (
  SELECT 1 AS StartPosition,
         ISNULL(NULLIF(CHARINDEX(@StringDelimiter, @Indexes, 1), 0), LEN(@Indexes) + 1) AS EndPosition,
         SUBSTRING(@Indexes, 1, ISNULL(NULLIF(CHARINDEX(@StringDelimiter, @Indexes, 1), 0), LEN(@Indexes) + 1) - 1) AS IndexItem
  WHERE @Indexes IS NOT NULL
  UNION ALL
  SELECT CAST(EndPosition AS int) + 1 AS StartPosition,
         ISNULL(NULLIF(CHARINDEX(@StringDelimiter, @Indexes, EndPosition + 1), 0), LEN(@Indexes) + 1) AS EndPosition,
         SUBSTRING(@Indexes, EndPosition + 1, ISNULL(NULLIF(CHARINDEX(@StringDelimiter, @Indexes, EndPosition + 1), 0), LEN(@Indexes) + 1) - EndPosition - 1) AS IndexItem
  FROM Indexes1
  WHERE EndPosition < LEN(@Indexes) + 1
  ),
  Indexes2 (IndexItem, StartPosition, Selected) AS
  (
  SELECT CASE WHEN IndexItem LIKE '-%' THEN RIGHT(IndexItem,LEN(IndexItem) - 1) ELSE IndexItem END AS IndexItem,
         StartPosition,
         CASE WHEN IndexItem LIKE '-%' THEN 0 ELSE 1 END AS Selected
  FROM Indexes1
  ),
  Indexes3 (IndexItem, StartPosition, Selected) AS
  (
  SELECT CASE WHEN IndexItem = 'ALL_INDEXES' THEN '%.%.%.%' ELSE IndexItem END AS IndexItem,
         StartPosition,
         Selected
  FROM Indexes2
  ),
  Indexes4 (DatabaseName, SchemaName, ObjectName, IndexName, StartPosition, Selected) AS
  (
  SELECT CASE WHEN PARSENAME(IndexItem,4) IS NULL THEN PARSENAME(IndexItem,3) ELSE PARSENAME(IndexItem,4) END AS DatabaseName,
         CASE WHEN PARSENAME(IndexItem,4) IS NULL THEN PARSENAME(IndexItem,2) ELSE PARSENAME(IndexItem,3) END AS SchemaName,
         CASE WHEN PARSENAME(IndexItem,4) IS NULL THEN PARSENAME(IndexItem,1) ELSE PARSENAME(IndexItem,2) END AS ObjectName,
         CASE WHEN PARSENAME(IndexItem,4) IS NULL THEN '%' ELSE PARSENAME(IndexItem,1) END AS IndexName,
         StartPosition,
         Selected
  FROM Indexes3
  )
  INSERT INTO @SelectedIndexes (DatabaseName, SchemaName, ObjectName, IndexName, StartPosition, Selected)
  SELECT DatabaseName, SchemaName, ObjectName, IndexName, StartPosition, Selected
  FROM Indexes4
  OPTION (MAXRECURSION 0)

  ----------------------------------------------------------------------------------------------------
  --// Select actions                                                                             //--
  ----------------------------------------------------------------------------------------------------

  SET @FragmentationLow = REPLACE(@FragmentationLow, @StringDelimiter + ' ', @StringDelimiter);

  WITH FragmentationLow (StartPosition, EndPosition, [Action]) AS
  (
  SELECT 1 AS StartPosition,
         ISNULL(NULLIF(CHARINDEX(@StringDelimiter, @FragmentationLow, 1), 0), LEN(@FragmentationLow) + 1) AS EndPosition,
         SUBSTRING(@FragmentationLow, 1, ISNULL(NULLIF(CHARINDEX(@StringDelimiter, @FragmentationLow, 1), 0), LEN(@FragmentationLow) + 1) - 1) AS [Action]
  WHERE @FragmentationLow IS NOT NULL
  UNION ALL
  SELECT CAST(EndPosition AS int) + 1 AS StartPosition,
         ISNULL(NULLIF(CHARINDEX(@StringDelimiter, @FragmentationLow, EndPosition + 1), 0), LEN(@FragmentationLow) + 1) AS EndPosition,
         SUBSTRING(@FragmentationLow, EndPosition + 1, ISNULL(NULLIF(CHARINDEX(@StringDelimiter, @FragmentationLow, EndPosition + 1), 0), LEN(@FragmentationLow) + 1) - EndPosition - 1) AS [Action]
  FROM FragmentationLow
  WHERE EndPosition < LEN(@FragmentationLow) + 1
  )
  INSERT INTO @ActionsPreferred(FragmentationGroup, [Priority], [Action])
  SELECT 'Low' AS FragmentationGroup,
         ROW_NUMBER() OVER(ORDER BY StartPosition ASC) AS [Priority],
         [Action]
  FROM FragmentationLow
  OPTION (MAXRECURSION 0)

  SET @FragmentationMedium = REPLACE(@FragmentationMedium, @StringDelimiter + ' ', @StringDelimiter);

  WITH FragmentationMedium (StartPosition, EndPosition, [Action]) AS
  (
  SELECT 1 AS StartPosition,
         ISNULL(NULLIF(CHARINDEX(@StringDelimiter, @FragmentationMedium, 1), 0), LEN(@FragmentationMedium) + 1) AS EndPosition,
         SUBSTRING(@FragmentationMedium, 1, ISNULL(NULLIF(CHARINDEX(@StringDelimiter, @FragmentationMedium, 1), 0), LEN(@FragmentationMedium) + 1) - 1) AS [Action]
  WHERE @FragmentationMedium IS NOT NULL
  UNION ALL
  SELECT CAST(EndPosition AS int) + 1 AS StartPosition,
         ISNULL(NULLIF(CHARINDEX(@StringDelimiter, @FragmentationMedium, EndPosition + 1), 0), LEN(@FragmentationMedium) + 1) AS EndPosition,
         SUBSTRING(@FragmentationMedium, EndPosition + 1, ISNULL(NULLIF(CHARINDEX(@StringDelimiter, @FragmentationMedium, EndPosition + 1), 0), LEN(@FragmentationMedium) + 1) - EndPosition - 1) AS [Action]
  FROM FragmentationMedium
  WHERE EndPosition < LEN(@FragmentationMedium) + 1
  )
  INSERT INTO @ActionsPreferred(FragmentationGroup, [Priority], [Action])
  SELECT 'Medium' AS FragmentationGroup,
         ROW_NUMBER() OVER(ORDER BY StartPosition ASC) AS [Priority],
         [Action]
  FROM FragmentationMedium
  OPTION (MAXRECURSION 0)

  SET @FragmentationHigh = REPLACE(@FragmentationHigh, @StringDelimiter + ' ', @StringDelimiter);

  WITH FragmentationHigh (StartPosition, EndPosition, [Action]) AS
  (
  SELECT 1 AS StartPosition,
         ISNULL(NULLIF(CHARINDEX(@StringDelimiter, @FragmentationHigh, 1), 0), LEN(@FragmentationHigh) + 1) AS EndPosition,
         SUBSTRING(@FragmentationHigh, 1, ISNULL(NULLIF(CHARINDEX(@StringDelimiter, @FragmentationHigh, 1), 0), LEN(@FragmentationHigh) + 1) - 1) AS [Action]
  WHERE @FragmentationHigh IS NOT NULL
  UNION ALL
  SELECT CAST(EndPosition AS int) + 1 AS StartPosition,
         ISNULL(NULLIF(CHARINDEX(@StringDelimiter, @FragmentationHigh, EndPosition + 1), 0), LEN(@FragmentationHigh) + 1) AS EndPosition,
         SUBSTRING(@FragmentationHigh, EndPosition + 1, ISNULL(NULLIF(CHARINDEX(@StringDelimiter, @FragmentationHigh, EndPosition + 1), 0), LEN(@FragmentationHigh) + 1) - EndPosition - 1) AS [Action]
  FROM FragmentationHigh
  WHERE EndPosition < LEN(@FragmentationHigh) + 1
  )
  INSERT INTO @ActionsPreferred(FragmentationGroup, [Priority], [Action])
  SELECT 'High' AS FragmentationGroup,
         ROW_NUMBER() OVER(ORDER BY StartPosition ASC) AS [Priority],
         [Action]
  FROM FragmentationHigh
  OPTION (MAXRECURSION 0)

  ----------------------------------------------------------------------------------------------------
  --// Check input parameters                                                                     //--
  ----------------------------------------------------------------------------------------------------

  IF EXISTS (SELECT [Action] FROM @ActionsPreferred WHERE FragmentationGroup = 'Low' AND [Action] NOT IN(SELECT * FROM @Actions))
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @FragmentationLow is not supported.', 16, 1
  END

  IF EXISTS (SELECT * FROM @ActionsPreferred WHERE FragmentationGroup = 'Low' GROUP BY [Action] HAVING COUNT(*) > 1)
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @FragmentationLow is not supported.', 16, 2
  END

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

  IF EXISTS (SELECT [Action] FROM @ActionsPreferred WHERE FragmentationGroup = 'Medium' AND [Action] NOT IN(SELECT * FROM @Actions))
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @FragmentationMedium is not supported.', 16, 1
  END

  IF EXISTS (SELECT * FROM @ActionsPreferred WHERE FragmentationGroup = 'Medium' GROUP BY [Action] HAVING COUNT(*) > 1)
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @FragmentationMedium is not supported.', 16, 2
  END

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

  IF EXISTS (SELECT [Action] FROM @ActionsPreferred WHERE FragmentationGroup = 'High' AND [Action] NOT IN(SELECT * FROM @Actions))
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @FragmentationHigh is not supported.', 16, 1
  END

  IF EXISTS (SELECT * FROM @ActionsPreferred WHERE FragmentationGroup = 'High' GROUP BY [Action] HAVING COUNT(*) > 1)
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @FragmentationHigh is not supported.', 16, 2
  END

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

  IF @FragmentationLevel1 <= 0 OR @FragmentationLevel1 >= 100 OR @FragmentationLevel1 IS NULL
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @FragmentationLevel1 is not supported.', 16, 1
  END

  IF @FragmentationLevel1 >= @FragmentationLevel2
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @FragmentationLevel1 is not supported.', 16, 2
  END

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

  IF @FragmentationLevel2 <= 0 OR @FragmentationLevel2 >= 100 OR @FragmentationLevel2 IS NULL
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @FragmentationLevel2 is not supported.', 16, 1
  END

  IF @FragmentationLevel2 <= @FragmentationLevel1
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @FragmentationLevel2 is not supported.', 16, 2
  END

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

  IF @MinNumberOfPages < 0 OR @MinNumberOfPages IS NULL
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @MinNumberOfPages is not supported.', 16, 1
  END

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

  IF @MaxNumberOfPages < 0
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @MaxNumberOfPages is not supported.', 16, 1
  END

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

  IF @SortInTempdb NOT IN('Y','N') OR @SortInTempdb IS NULL
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @SortInTempdb is not supported.', 16, 1
  END

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

  IF @MaxDOP < 0 OR @MaxDOP > 64
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @MaxDOP is not supported.', 16, 1
  END

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

  IF @FillFactor <= 0 OR @FillFactor > 100
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @FillFactor is not supported.', 16, 1
  END

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

  IF @PadIndex NOT IN('Y','N')
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @PadIndex is not supported.', 16, 1
  END

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

  IF @LOBCompaction NOT IN('Y','N') OR @LOBCompaction IS NULL
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @LOBCompaction is not supported.', 16, 1
  END

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

  IF @UpdateStatistics NOT IN('ALL','COLUMNS','INDEX')
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @UpdateStatistics is not supported.', 16, 1
  END

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

  IF @OnlyModifiedStatistics NOT IN('Y','N') OR @OnlyModifiedStatistics IS NULL
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @OnlyModifiedStatistics is not supported.', 16, 1
  END

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

  IF @StatisticsModificationLevel <= 0 OR @StatisticsModificationLevel > 100
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @StatisticsModificationLevel is not supported.', 16, 1
  END

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

  IF @OnlyModifiedStatistics = 'Y' AND @StatisticsModificationLevel IS NOT NULL
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'You can only specify one of the parameters @OnlyModifiedStatistics and @StatisticsModificationLevel.', 16, 1
  END

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

  IF @StatisticsSample <= 0 OR @StatisticsSample  > 100
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @StatisticsSample is not supported.', 16, 1
  END

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

  IF @StatisticsResample NOT IN('Y','N') OR @StatisticsResample IS NULL
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @StatisticsResample is not supported.', 16, 1
  END

  IF @StatisticsResample = 'Y' AND @StatisticsSample IS NOT NULL
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @StatisticsResample is not supported.', 16, 2
  END

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

  IF @PartitionLevel NOT IN('Y','N') OR @PartitionLevel IS NULL
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @PartitionLevel is not supported.', 16, 1
  END

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

  IF @MSShippedObjects NOT IN('Y','N') OR @MSShippedObjects IS NULL
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @MSShippedObjects is not supported.', 16, 1
  END

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

  IF EXISTS(SELECT * FROM @SelectedIndexes WHERE DatabaseName IS NULL OR SchemaName IS NULL OR ObjectName IS NULL OR IndexName IS NULL)
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @Indexes is not supported.', 16, 1
  END

  IF @Indexes IS NOT NULL AND NOT EXISTS(SELECT * FROM @SelectedIndexes)
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @Indexes is not supported.', 16, 2
  END

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

  IF @TimeLimit < 0
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @TimeLimit is not supported.', 16, 1
  END

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

  IF @Delay < 0
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @Delay is not supported.', 16, 1
  END

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

  IF @WaitAtLowPriorityMaxDuration < 0
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @WaitAtLowPriorityMaxDuration is not supported.', 16, 1
  END

  IF @WaitAtLowPriorityMaxDuration IS NOT NULL AND @Version < 12
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @WaitAtLowPriorityMaxDuration is not supported.', 16, 2
  END

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

  IF @WaitAtLowPriorityAbortAfterWait NOT IN('NONE','SELF','BLOCKERS')
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @WaitAtLowPriorityAbortAfterWait is not supported.', 16, 1
  END

  IF @WaitAtLowPriorityAbortAfterWait IS NOT NULL AND @Version < 12
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @WaitAtLowPriorityAbortAfterWait is not supported.', 16, 2
  END

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

  IF (@WaitAtLowPriorityAbortAfterWait IS NOT NULL AND @WaitAtLowPriorityMaxDuration IS NULL) OR (@WaitAtLowPriorityAbortAfterWait IS NULL AND @WaitAtLowPriorityMaxDuration IS NOT NULL)
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The parameters @WaitAtLowPriorityMaxDuration and @WaitAtLowPriorityAbortAfterWait can only be used together.', 16, 1
  END

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

  IF @Resumable NOT IN('Y','N') OR @Resumable IS NULL
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @Resumable is not supported.', 16, 1
  END

  IF @Resumable = 'Y' AND NOT (@Version >= 14 OR SERVERPROPERTY('EngineEdition') IN (5, 8))
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @Resumable is not supported.', 16, 2
  END

  IF @Resumable = 'Y' AND @SortInTempdb = 'Y'
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'You can only specify one of the parameters @Resumable and @SortInTempdb.', 16, 3
  END

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

  IF @LockTimeout < 0
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @LockTimeout is not supported.', 16, 1
  END

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

  IF @LockMessageSeverity NOT IN(10, 16)
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @LockMessageSeverity is not supported.', 16, 1
  END

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

  IF @StringDelimiter IS NULL OR LEN(@StringDelimiter) > 1
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @StringDelimiter is not supported.', 16, 1
  END

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

  IF @DatabaseOrder NOT IN('DATABASE_NAME_ASC','DATABASE_NAME_DESC','DATABASE_SIZE_ASC','DATABASE_SIZE_DESC')
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @DatabaseOrder is not supported.', 16, 1
  END

  IF @DatabaseOrder IS NOT NULL AND SERVERPROPERTY('EngineEdition') = 5
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @DatabaseOrder is not supported.', 16, 2
  END

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

  IF @DatabasesInParallel NOT IN('Y','N') OR @DatabasesInParallel IS NULL
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @DatabasesInParallel is not supported.', 16, 1
  END

  IF @DatabasesInParallel = 'Y' AND SERVERPROPERTY('EngineEdition') = 5
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @DatabasesInParallel is not supported.', 16, 2
  END

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

  IF LEN(@ExecuteAsUser) > 128
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @ExecuteAsUser is not supported.', 16, 1
  END

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

  IF @LogToTable NOT IN('Y','N') OR @LogToTable IS NULL
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @LogToTable is not supported.', 16, 1
  END

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

  IF @Execute NOT IN('Y','N') OR @Execute IS NULL
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The value for the parameter @Execute is not supported.', 16, 1
  END

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

  IF EXISTS(SELECT * FROM @Errors)
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The documentation is available at https://ola.hallengren.com/sql-server-index-and-statistics-maintenance.html.', 16, 1
  END

  ----------------------------------------------------------------------------------------------------
  --// Check that selected databases and availability groups exist                                //--
  ----------------------------------------------------------------------------------------------------

  SET @ErrorMessage = ''
  SELECT @ErrorMessage = @ErrorMessage + QUOTENAME(DatabaseName) + ', '
  FROM @SelectedDatabases
  WHERE DatabaseName NOT LIKE '%[%]%'
  AND DatabaseName NOT IN (SELECT DatabaseName FROM @tmpDatabases)
  IF @@ROWCOUNT > 0
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The following databases in the @Databases parameter do not exist: ' + LEFT(@ErrorMessage,LEN(@ErrorMessage)-1) + '.', 10, 1
  END

  SET @ErrorMessage = ''
  SELECT @ErrorMessage = @ErrorMessage + QUOTENAME(DatabaseName) + ', '
  FROM @SelectedIndexes
  WHERE DatabaseName NOT LIKE '%[%]%'
  AND DatabaseName NOT IN (SELECT DatabaseName FROM @tmpDatabases)
  IF @@ROWCOUNT > 0
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The following databases in the @Indexes parameter do not exist: ' + LEFT(@ErrorMessage,LEN(@ErrorMessage)-1) + '.', 10, 1
  END

  SET @ErrorMessage = ''
  SELECT @ErrorMessage = @ErrorMessage + QUOTENAME(AvailabilityGroupName) + ', '
  FROM @SelectedAvailabilityGroups
  WHERE AvailabilityGroupName NOT LIKE '%[%]%'
  AND AvailabilityGroupName NOT IN (SELECT AvailabilityGroupName FROM @tmpAvailabilityGroups)
  IF @@ROWCOUNT > 0
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The following availability groups do not exist: ' + LEFT(@ErrorMessage,LEN(@ErrorMessage)-1) + '.', 10, 1
  END

  SET @ErrorMessage = ''
  SELECT @ErrorMessage = @ErrorMessage + QUOTENAME(DatabaseName) + ', '
  FROM @SelectedIndexes
  WHERE DatabaseName NOT LIKE '%[%]%'
  AND DatabaseName IN (SELECT DatabaseName FROM @tmpDatabases)
  AND DatabaseName NOT IN (SELECT DatabaseName FROM @tmpDatabases WHERE Selected = 1)
  IF @@ROWCOUNT > 0
  BEGIN
    INSERT INTO @Errors ([Message], Severity, [State])
    SELECT 'The following databases have been selected in the @Indexes parameter, but not in the @Databases or @AvailabilityGroups parameters: ' + LEFT(@ErrorMessage,LEN(@ErrorMessage)-1) + '.', 10, 1
  END

  ----------------------------------------------------------------------------------------------------
  --// Raise errors                                                                               //--
  ----------------------------------------------------------------------------------------------------

  DECLARE ErrorCursor CURSOR FAST_FORWARD FOR SELECT [Message], Severity, [State] FROM @Errors ORDER BY [ID] ASC

  OPEN ErrorCursor

  FETCH ErrorCursor INTO @CurrentMessage, @CurrentSeverity, @CurrentState

  WHILE @@FETCH_STATUS = 0
  BEGIN
    RAISERROR('%s', @CurrentSeverity, @CurrentState, @CurrentMessage) WITH NOWAIT
    RAISERROR(@EmptyLine, 10, 1) WITH NOWAIT

    FETCH NEXT FROM ErrorCursor INTO @CurrentMessage, @CurrentSeverity, @CurrentState
  END

  CLOSE ErrorCursor

  DEALLOCATE ErrorCursor

  IF EXISTS (SELECT * FROM @Errors WHERE Severity >= 16)
  BEGIN
    SET @ReturnCode = 50000
    GOTO Logging
  END

  ----------------------------------------------------------------------------------------------------
  --// Should statistics be updated on the partition level?                                       //--
  ----------------------------------------------------------------------------------------------------

  SET @PartitionLevelStatistics = CASE WHEN @PartitionLevel = 'Y' AND ((@Version >= 12.05 AND @Version < 13) OR @Version >= 13.04422 OR SERVERPROPERTY('EngineEdition') IN (5,8)) THEN 1 ELSE 0 END

  ----------------------------------------------------------------------------------------------------
  --// Update database order                                                                      //--
  ----------------------------------------------------------------------------------------------------

  IF @DatabaseOrder IN('DATABASE_SIZE_ASC','DATABASE_SIZE_DESC')
  BEGIN
    UPDATE tmpDatabases
    SET DatabaseSize = (SELECT SUM(CAST(size AS bigint)) FROM sys.master_files WHERE [type] = 0 AND database_id = DB_ID(tmpDatabases.DatabaseName))
    FROM @tmpDatabases tmpDatabases
  END

  IF @DatabaseOrder IS NULL
  BEGIN
    WITH tmpDatabases AS (
    SELECT DatabaseName, [Order], ROW_NUMBER() OVER (ORDER BY StartPosition ASC, DatabaseName ASC) AS RowNumber
    FROM @tmpDatabases tmpDatabases
    WHERE Selected = 1
    )
    UPDATE tmpDatabases
    SET [Order] = RowNumber
  END
  ELSE
  IF @DatabaseOrder = 'DATABASE_NAME_ASC'
  BEGIN
    WITH tmpDatabases AS (
    SELECT DatabaseName, [Order], ROW_NUMBER() OVER (ORDER BY DatabaseName ASC) AS RowNumber
    FROM @tmpDatabases tmpDatabases
    WHERE Selected = 1
    )
    UPDATE tmpDatabases
    SET [Order] = RowNumber
  END
  ELSE
  IF @DatabaseOrder = 'DATABASE_NAME_DESC'
  BEGIN
    WITH tmpDatabases AS (
    SELECT DatabaseName, [Order], ROW_NUMBER() OVER (ORDER BY DatabaseName DESC) AS RowNumber
    FROM @tmpDatabases tmpDatabases
    WHERE Selected = 1
    )
    UPDATE tmpDatabases
    SET [Order] = RowNumber
  END
  ELSE
  IF @DatabaseOrder = 'DATABASE_SIZE_ASC'
  BEGIN
    WITH tmpDatabases AS (
    SELECT DatabaseName, [Order], ROW_NUMBER() OVER (ORDER BY DatabaseSize ASC) AS RowNumber
    FROM @tmpDatabases tmpDatabases
    WHERE Selected = 1
    )
    UPDATE tmpDatabases
    SET [Order] = RowNumber
  END
  ELSE
  IF @DatabaseOrder = 'DATABASE_SIZE_DESC'
  BEGIN
    WITH tmpDatabases AS (
    SELECT DatabaseName, [Order], ROW_NUMBER() OVER (ORDER BY DatabaseSize DESC) AS RowNumber
    FROM @tmpDatabases tmpDatabases
    WHERE Selected = 1
    )
    UPDATE tmpDatabases
    SET [Order] = RowNumber
  END

  ----------------------------------------------------------------------------------------------------
  --// Update the queue                                                                           //--
  ----------------------------------------------------------------------------------------------------

  IF @DatabasesInParallel = 'Y'
  BEGIN

    BEGIN TRY

      SELECT @QueueID = QueueID
      FROM dbo.[Queue]
      WHERE SchemaName = @SchemaName
      AND ObjectName = @ObjectName
      AND [Parameters] = @Parameters

      IF @QueueID IS NULL
      BEGIN
        BEGIN TRANSACTION

        SELECT @QueueID = QueueID
        FROM dbo.[Queue] WITH (UPDLOCK, HOLDLOCK)
        WHERE SchemaName = @SchemaName
        AND ObjectName = @ObjectName
        AND [Parameters] = @Parameters

        IF @QueueID IS NULL
        BEGIN
          INSERT INTO dbo.[Queue] (SchemaName, ObjectName, [Parameters])
          SELECT @SchemaName, @ObjectName, @Parameters

          SET @QueueID = SCOPE_IDENTITY()
        END

        COMMIT TRANSACTION
      END

      BEGIN TRANSACTION

      UPDATE [Queue]
      SET QueueStartTime = SYSDATETIME(),
          SessionID = @@SPID,
          RequestID = (SELECT request_id FROM sys.dm_exec_requests WHERE session_id = @@SPID),
          RequestStartTime = (SELECT start_time FROM sys.dm_exec_requests WHERE session_id = @@SPID)
      FROM dbo.[Queue] [Queue]
      WHERE QueueID = @QueueID
      AND NOT EXISTS (SELECT *
                      FROM sys.dm_exec_requests
                      WHERE session_id = [Queue].SessionID
                      AND request_id = [Queue].RequestID
                      AND start_time = [Queue].RequestStartTime)
      AND NOT EXISTS (SELECT *
                      FROM dbo.QueueDatabase QueueDatabase
                      INNER JOIN sys.dm_exec_requests ON QueueDatabase.SessionID = session_id AND QueueDatabase.RequestID = request_id AND QueueDatabase.RequestStartTime = start_time
                      WHERE QueueDatabase.QueueID = @QueueID)

      IF @@ROWCOUNT = 1
      BEGIN
        INSERT INTO dbo.QueueDatabase (QueueID, DatabaseName)
        SELECT @QueueID AS QueueID,
               DatabaseName
        FROM @tmpDatabases tmpDatabases
        WHERE Selected = 1
        AND NOT EXISTS (SELECT * FROM dbo.QueueDatabase WHERE DatabaseName = tmpDatabases.DatabaseName AND QueueID = @QueueID)

        DELETE QueueDatabase
        FROM dbo.QueueDatabase QueueDatabase
        WHERE QueueID = @QueueID
        AND NOT EXISTS (SELECT * FROM @tmpDatabases tmpDatabases WHERE DatabaseName = QueueDatabase.DatabaseName AND Selected = 1)

        UPDATE QueueDatabase
        SET DatabaseOrder = tmpDatabases.[Order]
        FROM dbo.QueueDatabase QueueDatabase
        INNER JOIN @tmpDatabases tmpDatabases ON QueueDatabase.DatabaseName = tmpDatabases.DatabaseName
        WHERE QueueID = @QueueID
      END

      COMMIT TRANSACTION

      SELECT @QueueStartTime = QueueStartTime
      FROM dbo.[Queue]
      WHERE QueueID = @QueueID

    END TRY

    BEGIN CATCH
      IF XACT_STATE() <> 0
      BEGIN
        ROLLBACK TRANSACTION
      END
      SET @ErrorMessage = 'Msg ' + CAST(ERROR_NUMBER() AS nvarchar) + ', ' + ISNULL(ERROR_MESSAGE(),'')
      RAISERROR('%s',16,1,@ErrorMessage) WITH NOWAIT
      RAISERROR(@EmptyLine,10,1) WITH NOWAIT
      SET @ReturnCode = ERROR_NUMBER()
      GOTO Logging
    END CATCH

  END

  ----------------------------------------------------------------------------------------------------
  --// Execute commands                                                                           //--
  ----------------------------------------------------------------------------------------------------

  WHILE (1 = 1)
  BEGIN

    IF @DatabasesInParallel = 'Y'
    BEGIN
      UPDATE QueueDatabase
      SET DatabaseStartTime = NULL,
          SessionID = NULL,
          RequestID = NULL,
          RequestStartTime = NULL
      FROM dbo.QueueDatabase QueueDatabase
      WHERE QueueID = @QueueID
      AND DatabaseStartTime IS NOT NULL
      AND DatabaseEndTime IS NULL
      AND NOT EXISTS (SELECT * FROM sys.dm_exec_requests WHERE session_id = QueueDatabase.SessionID AND request_id = QueueDatabase.RequestID AND start_time = QueueDatabase.RequestStartTime)

      UPDATE QueueDatabase
      SET DatabaseStartTime = SYSDATETIME(),
          DatabaseEndTime = NULL,
          SessionID = @@SPID,
          RequestID = (SELECT request_id FROM sys.dm_exec_requests WHERE session_id = @@SPID),
          RequestStartTime = (SELECT start_time FROM sys.dm_exec_requests WHERE session_id = @@SPID),
          @CurrentDatabaseName = DatabaseName
      FROM (SELECT TOP 1 DatabaseStartTime,
                         DatabaseEndTime,
                         SessionID,
                         RequestID,
                         RequestStartTime,
                         DatabaseName
            FROM dbo.QueueDatabase
            WHERE QueueID = @QueueID
            AND (DatabaseStartTime < @QueueStartTime OR DatabaseStartTime IS NULL)
            AND NOT (DatabaseStartTime IS NOT NULL AND DatabaseEndTime IS NULL)
            ORDER BY DatabaseOrder ASC
            ) QueueDatabase
    END
    ELSE
    BEGIN
      SELECT TOP 1 @CurrentDBID = ID,
                   @CurrentDatabaseName = DatabaseName
      FROM @tmpDatabases
      WHERE Selected = 1
      AND Completed = 0
      ORDER BY [Order] ASC
    END

    IF @@ROWCOUNT = 0
    BEGIN
      BREAK
    END

    SET @CurrentDatabase_sp_executesql = QUOTENAME(@CurrentDatabaseName) + '.sys.sp_executesql'

    BEGIN
      SET @DatabaseMessage = 'Date and time: ' + CONVERT(nvarchar,SYSDATETIME(),120)
      RAISERROR('%s',10,1,@DatabaseMessage) WITH NOWAIT

      SET @DatabaseMessage = 'Database: ' + QUOTENAME(@CurrentDatabaseName)
      RAISERROR('%s',10,1,@DatabaseMessage) WITH NOWAIT
    END

    SELECT @CurrentUserAccess = user_access_desc,
           @CurrentIsReadOnly = is_read_only,
           @CurrentDatabaseState = state_desc,
           @CurrentInStandby = is_in_standby,
           @CurrentRecoveryModel = recovery_model_desc
    FROM sys.databases
    WHERE [name] = @CurrentDatabaseName

    BEGIN
      SET @DatabaseMessage = 'State: ' + @CurrentDatabaseState
      RAISERROR('%s',10,1,@DatabaseMessage) WITH NOWAIT

      SET @DatabaseMessage = 'Standby: ' + CASE WHEN @CurrentInStandby = 1 THEN 'Yes' ELSE 'No' END
      RAISERROR('%s',10,1,@DatabaseMessage) WITH NOWAIT

      SET @DatabaseMessage = 'Updateability: ' + CASE WHEN @CurrentIsReadOnly = 1 THEN 'READ_ONLY' WHEN  @CurrentIsReadOnly = 0 THEN 'READ_WRITE' END
      RAISERROR('%s',10,1,@DatabaseMessage) WITH NOWAIT

      SET @DatabaseMessage = 'User access: ' + @CurrentUserAccess
      RAISERROR('%s',10,1,@DatabaseMessage) WITH NOWAIT

      SET @DatabaseMessage = 'Recovery model: ' + @CurrentRecoveryModel
      RAISERROR('%s',10,1,@DatabaseMessage) WITH NOWAIT
    END

    IF @Version >= 11 AND SERVERPROPERTY('IsHadrEnabled') = 1
    BEGIN
      SELECT @CurrentReplicaID = databases.replica_id
      FROM sys.databases databases
      INNER JOIN sys.availability_replicas availability_replicas ON databases.replica_id = availability_replicas.replica_id
      WHERE databases.[name] = @CurrentDatabaseName

      SELECT @CurrentAvailabilityGroupID = group_id
      FROM sys.availability_replicas
      WHERE replica_id = @CurrentReplicaID

      SELECT @CurrentAvailabilityGroupRole = role_desc
      FROM sys.dm_hadr_availability_replica_states
      WHERE replica_id = @CurrentReplicaID

      SELECT @CurrentAvailabilityGroup = [name]
      FROM sys.availability_groups
      WHERE group_id = @CurrentAvailabilityGroupID
    END

    IF SERVERPROPERTY('EngineEdition') <> 5
    BEGIN
      SELECT @CurrentDatabaseMirroringRole = UPPER(mirroring_role_desc)
      FROM sys.database_mirroring database_mirroring
      INNER JOIN sys.databases databases ON database_mirroring.database_id = databases.database_id
      WHERE databases.[name] = @CurrentDatabaseName
    END

    IF @CurrentAvailabilityGroup IS NOT NULL
    BEGIN
      SET @DatabaseMessage = 'Availability group: ' + ISNULL(@CurrentAvailabilityGroup,'N/A')
      RAISERROR('%s',10,1,@DatabaseMessage) WITH NOWAIT

      SET @DatabaseMessage = 'Availability group role: ' + ISNULL(@CurrentAvailabilityGroupRole,'N/A')
      RAISERROR('%s',10,1,@DatabaseMessage) WITH NOWAIT
    END

    IF @CurrentDatabaseMirroringRole IS NOT NULL
    BEGIN
      SET @DatabaseMessage = 'Database mirroring role: ' + @CurrentDatabaseMirroringRole
      RAISERROR('%s',10,1,@DatabaseMessage) WITH NOWAIT
    END

    RAISERROR(@EmptyLine,10,1) WITH NOWAIT

    IF @ExecuteAsUser IS NOT NULL
    AND @CurrentDatabaseState = 'ONLINE'
    AND NOT (@CurrentUserAccess = 'SINGLE_USER')
    AND NOT (@CurrentAvailabilityGroup IS NOT NULL AND (@CurrentAvailabilityGroupRole <> 'PRIMARY' OR @CurrentAvailabilityGroupRole IS NULL))
    AND NOT (@AmazonRDS = 1 AND @CurrentDatabaseName = 'rdsadmin')
    BEGIN
      SET @CurrentCommand = ''
      SET @CurrentCommand += 'IF EXISTS(SELECT * FROM sys.database_principals database_principals WHERE database_principals.[name] = @ParamExecuteAsUser) BEGIN SET @ParamExecuteAsUserExists = 1 END ELSE BEGIN SET @ParamExecuteAsUserExists = 0 END'

      EXECUTE @CurrentDatabase_sp_executesql @stmt = @CurrentCommand, @params = N'@ParamExecuteAsUser sysname, @ParamExecuteAsUserExists bit OUTPUT', @ParamExecuteAsUser = @ExecuteAsUser, @ParamExecuteAsUserExists = @CurrentExecuteAsUserExists OUTPUT
    END

    IF @CurrentExecuteAsUserExists = 0
    BEGIN
      SET @DatabaseMessage = 'The user ' + QUOTENAME(@ExecuteAsUser) + ' does not exist in the database ' + QUOTENAME(@CurrentDatabaseName) + '.'
      RAISERROR('%s',16,1,@DatabaseMessage) WITH NOWAIT
      RAISERROR(@EmptyLine,10,1) WITH NOWAIT
    END

    IF @CurrentDatabaseState = 'ONLINE'
    AND NOT (@CurrentUserAccess = 'SINGLE_USER')
    AND NOT (@CurrentAvailabilityGroup IS NOT NULL AND (@CurrentAvailabilityGroupRole <> 'PRIMARY' OR @CurrentAvailabilityGroupRole IS NULL))
    AND NOT (@AmazonRDS = 1 AND @CurrentDatabaseName = 'rdsadmin')
    AND NOT (@CurrentIsReadOnly = 1)
    AND (@CurrentExecuteAsUserExists = 1 OR @CurrentExecuteAsUserExists IS NULL)
    BEGIN

      -- Select indexes in the current database
      IF (EXISTS(SELECT * FROM @ActionsPreferred) OR @UpdateStatistics IS NOT NULL) AND (SYSDATETIME() < DATEADD(SECOND,@TimeLimit,@StartTime) OR @TimeLimit IS NULL)
      BEGIN
        SET @CurrentCommand = 'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'
                              + ' SELECT SchemaID, SchemaName, ObjectID, ObjectName, ObjectType, IsMemoryOptimized, IndexID, IndexName, IndexType, AllowPageLocks, HasFilter, IsImageText, IsNewLOB, IsFileStream, HasClusteredColumnstore, HasNonClusteredColumnstore, IsComputed, IsClusteredIndexComputed, IsTimestamp, OnReadOnlyFileGroup, ResumableIndexOperation, StatisticsID, StatisticsName, NoRecompute, IsIncremental, PartitionID, PartitionNumber, PartitionCount, [Order], Selected, Completed'
                              + ' FROM ('

        IF EXISTS(SELECT * FROM @ActionsPreferred) OR @UpdateStatistics IN('ALL','INDEX')
        BEGIN
          SET @CurrentCommand = @CurrentCommand + 'SELECT schemas.[schema_id] AS SchemaID'
                                                    + ', schemas.[name] AS SchemaName'
                                                    + ', objects.[object_id] AS ObjectID'
                                                    + ', objects.[name] AS ObjectName'
                                                    + ', RTRIM(objects.[type]) AS ObjectType'
                                                    + ', ' + CASE WHEN @Version >= 12 THEN 'tables.is_memory_optimized' ELSE '0' END + ' AS IsMemoryOptimized'
                                                    + ', indexes.index_id AS IndexID'
                                                    + ', indexes.[name] AS IndexName'
                                                    + ', indexes.[type] AS IndexType'
                                                    + ', indexes.allow_page_locks AS AllowPageLocks'
                                                    + ', indexes.has_filter AS HasFilter'

                                                    + ', CASE WHEN indexes.[type] = 1 AND EXISTS(SELECT * FROM sys.columns columns INNER JOIN sys.types types ON columns.system_type_id = types.user_type_id WHERE columns.[object_id] = objects.object_id AND types.name IN(''image'',''text'',''ntext'')) THEN 1 ELSE 0 END AS IsImageText'

                                                    + ', CASE WHEN indexes.[type] = 1 AND EXISTS(SELECT * FROM sys.columns columns INNER JOIN sys.types types ON columns.system_type_id = types.user_type_id OR (columns.user_type_id = types.user_type_id AND types.is_assembly_type = 1) WHERE columns.[object_id] = objects.object_id AND (types.name IN(''xml'') OR (types.name IN(''varchar'',''nvarchar'',''varbinary'') AND columns.max_length = -1) OR (types.is_assembly_type = 1 AND columns.max_length = -1))) THEN 1'
                                                    + ' WHEN indexes.[type] = 2 AND EXISTS(SELECT * FROM sys.index_columns index_columns INNER JOIN sys.columns columns ON index_columns.[object_id] = columns.[object_id] AND index_columns.column_id = columns.column_id INNER JOIN sys.types types ON columns.system_type_id = types.user_type_id OR (columns.user_type_id = types.user_type_id AND types.is_assembly_type = 1) WHERE index_columns.[object_id] = objects.object_id AND index_columns.index_id = indexes.index_id AND (types.[name] IN(''xml'') OR (types.[name] IN(''varchar'',''nvarchar'',''varbinary'') AND columns.max_length = -1) OR (types.is_assembly_type = 1 AND columns.max_length = -1))) THEN 1 ELSE 0 END AS IsNewLOB'

                                                    + ', CASE WHEN indexes.[type] = 1 AND EXISTS(SELECT * FROM sys.columns columns WHERE columns.[object_id] = objects.object_id  AND columns.is_filestream = 1) THEN 1 ELSE 0 END AS IsFileStream'

                                                    + ', CASE WHEN EXISTS(SELECT * FROM sys.indexes indexes WHERE indexes.[object_id] = objects.object_id AND [type] = 5) THEN 1 ELSE 0 END AS HasClusteredColumnstore'

                                                    + ', CASE WHEN EXISTS(SELECT * FROM sys.indexes indexes WHERE indexes.[object_id] = objects.object_id AND [type] = 6) THEN 1 ELSE 0 END AS HasNonClusteredColumnstore'

                                                    + ', CASE WHEN EXISTS(SELECT * FROM sys.index_columns index_columns INNER JOIN sys.columns columns ON index_columns.object_id = columns.object_id AND index_columns.column_id = columns.column_id WHERE (index_columns.key_ordinal > 0 OR index_columns.partition_ordinal > 0) AND columns.is_computed = 1 AND index_columns.object_id = indexes.object_id AND index_columns.index_id = indexes.index_id) THEN 1 ELSE 0 END AS IsComputed'

                                                    + ', CASE WHEN EXISTS(SELECT * FROM sys.index_columns index_columns INNER JOIN sys.columns columns ON index_columns.object_id = columns.object_id AND index_columns.column_id = columns.column_id INNER JOIN sys.indexes indexes2 ON index_columns.object_id = indexes2.object_id AND index_columns.index_id = indexes2.index_id WHERE (index_columns.key_ordinal > 0 OR index_columns.partition_ordinal > 0) AND columns.is_computed = 1 AND indexes2.[type] = 1 AND index_columns.object_id = indexes.object_id) THEN 1 ELSE 0 END AS IsClusteredIndexComputed'

                                                    + ', CASE WHEN EXISTS(SELECT * FROM sys.index_columns index_columns INNER JOIN sys.columns columns ON index_columns.[object_id] = columns.[object_id] AND index_columns.column_id = columns.column_id INNER JOIN sys.types types ON columns.system_type_id = types.system_type_id WHERE index_columns.[object_id] = objects.object_id AND index_columns.index_id = indexes.index_id AND types.[name] = ''timestamp'') THEN 1 ELSE 0 END AS IsTimestamp'

                                                    + ', CASE WHEN EXISTS (SELECT * FROM sys.indexes indexes2 INNER JOIN sys.destination_data_spaces destination_data_spaces ON indexes.data_space_id = destination_data_spaces.partition_scheme_id INNER JOIN sys.filegroups filegroups ON destination_data_spaces.data_space_id = filegroups.data_space_id WHERE filegroups.is_read_only = 1 AND indexes2.[object_id] = indexes.[object_id] AND indexes2.[index_id] = indexes.index_id' + CASE WHEN @PartitionLevel = 'Y' THEN ' AND destination_data_spaces.destination_id = partitions.partition_number' ELSE '' END + ') THEN 1'
                                                    + ' WHEN EXISTS (SELECT * FROM sys.indexes indexes2 INNER JOIN sys.filegroups filegroups ON indexes.data_space_id = filegroups.data_space_id WHERE filegroups.is_read_only = 1 AND indexes.[object_id] = indexes2.[object_id] AND indexes.[index_id] = indexes2.index_id) THEN 1'
                                                    + ' WHEN indexes.[type] = 1 AND EXISTS (SELECT * FROM sys.tables tables INNER JOIN sys.filegroups filegroups ON tables.lob_data_space_id = filegroups.data_space_id WHERE filegroups.is_read_only = 1 AND tables.[object_id] = objects.[object_id]) THEN 1 ELSE 0 END AS OnReadOnlyFileGroup'

                                                    + ', ' + CASE WHEN @Version >= 14 OR SERVERPROPERTY('EngineEdition') IN (5, 8) THEN 'CASE WHEN EXISTS(SELECT * FROM sys.index_resumable_operations index_resumable_operations WHERE state_desc = ''PAUSED'' AND index_resumable_operations.object_id = indexes.object_id AND index_resumable_operations.index_id = indexes.index_id' + CASE WHEN @PartitionLevel = 'Y' THEN ' AND (index_resumable_operations.partition_number = partitions.partition_number OR index_resumable_operations.partition_number IS NULL)' ELSE '' END + ') THEN 1 ELSE 0 END' ELSE '0' END + ' AS ResumableIndexOperation'

                                                    + ', stats.stats_id AS StatisticsID'
                                                    + ', stats.name AS StatisticsName'
                                                    + ', stats.no_recompute AS NoRecompute'
                                                    + ', ' + CASE WHEN @Version >= 12 THEN 'stats.is_incremental' ELSE '0' END + ' AS IsIncremental'
                                                    + ', ' + CASE WHEN @PartitionLevel = 'Y' THEN 'partitions.partition_id AS PartitionID' WHEN @PartitionLevel = 'N' THEN 'NULL AS PartitionID' END
                                                    + ', ' + CASE WHEN @PartitionLevel = 'Y' THEN 'partitions.partition_number AS PartitionNumber' WHEN @PartitionLevel = 'N' THEN 'NULL AS PartitionNumber' END
                                                    + ', ' + CASE WHEN @PartitionLevel = 'Y' THEN 'IndexPartitions.partition_count AS PartitionCount' WHEN @PartitionLevel = 'N' THEN 'NULL AS PartitionCount' END
                                                    + ', 0 AS [Order]'
                                                    + ', 0 AS Selected'
                                                    + ', 0 AS Completed'
                                                    + ' FROM sys.indexes indexes'
                                                    + ' INNER JOIN sys.objects objects ON indexes.[object_id] = objects.[object_id]'
                                                    + ' INNER JOIN sys.schemas schemas ON objects.[schema_id] = schemas.[schema_id]'
                                                    + ' LEFT OUTER JOIN sys.tables tables ON objects.[object_id] = tables.[object_id]'
                                                    + ' LEFT OUTER JOIN sys.stats stats ON indexes.[object_id] = stats.[object_id] AND indexes.[index_id] = stats.[stats_id]'
          IF @PartitionLevel = 'Y'
          BEGIN
            SET @CurrentCommand = @CurrentCommand + ' LEFT OUTER JOIN sys.partitions partitions ON indexes.[object_id] = partitions.[object_id] AND indexes.index_id = partitions.index_id'
                                                      + ' LEFT OUTER JOIN (SELECT partitions.[object_id], partitions.index_id, COUNT(DISTINCT partitions.partition_number) AS partition_count FROM sys.partitions partitions GROUP BY partitions.[object_id], partitions.index_id) IndexPartitions ON partitions.[object_id] = IndexPartitions.[object_id] AND partitions.[index_id] = IndexPartitions.[index_id]'
          END

          SET @CurrentCommand = @CurrentCommand + ' WHERE objects.[type] IN(''U'',''V'')'
                                                    + CASE WHEN @MSShippedObjects = 'N' THEN ' AND objects.is_ms_shipped = 0' ELSE '' END
                                                    + ' AND indexes.[type] IN(1,2,3,4,5,6,7)'
                                                    + ' AND indexes.is_disabled = 0 AND indexes.is_hypothetical = 0'
        END

        IF (EXISTS(SELECT * FROM @ActionsPreferred) AND @UpdateStatistics = 'COLUMNS') OR @UpdateStatistics = 'ALL'
        BEGIN
          SET @CurrentCommand = @CurrentCommand + ' UNION '
        END

        IF @UpdateStatistics IN('ALL','COLUMNS')
        BEGIN
          SET @CurrentCommand = @CurrentCommand + 'SELECT schemas.[schema_id] AS SchemaID'
                                                    + ', schemas.[name] AS SchemaName'
                                                    + ', objects.[object_id] AS ObjectID'
                                                    + ', objects.[name] AS ObjectName'
                                                    + ', RTRIM(objects.[type]) AS ObjectType'
                                                    + ', ' + CASE WHEN @Version >= 12 THEN 'tables.is_memory_optimized' ELSE '0' END + ' AS IsMemoryOptimized'
                                                    + ', NULL AS IndexID, NULL AS IndexName'
                                                    + ', NULL AS IndexType'
                                                    + ', NULL AS AllowPageLocks'
                                                    + ', NULL AS HasFilter'
                                                    + ', NULL AS IsImageText'
                                                    + ', NULL AS IsNewLOB'
                                                    + ', NULL AS IsFileStream'
                                                    + ', NULL AS HasClusteredColumnstore'
                                                    + ', NULL AS HasNonClusteredColumnstore'
                                                    + ', NULL AS IsComputed'
                                                    + ', NULL AS IsClusteredIndexComputed'
                                                    + ', NULL AS IsTimestamp'
                                                    + ', NULL AS OnReadOnlyFileGroup'
                                                    + ', NULL AS ResumableIndexOperation'
                                                    + ', stats.stats_id AS StatisticsID'
                                                    + ', stats.name AS StatisticsName'
                                                    + ', stats.no_recompute AS NoRecompute'
                                                    + ', ' + CASE WHEN @Version >= 12 THEN 'stats.is_incremental' ELSE '0' END + ' AS IsIncremental'
                                                    + ', NULL AS PartitionID'
                                                    + ', ' + CASE WHEN @PartitionLevelStatistics = 1 THEN 'dm_db_incremental_stats_properties.partition_number' ELSE 'NULL' END + ' AS PartitionNumber'
                                                    + ', NULL AS PartitionCount'
                                                    + ', 0 AS [Order]'
                                                    + ', 0 AS Selected'
                                                    + ', 0 AS Completed'
                                                    + ' FROM sys.stats stats'
                                                    + ' INNER JOIN sys.objects objects ON stats.[object_id] = objects.[object_id]'
                                                    + ' INNER JOIN sys.schemas schemas ON objects.[schema_id] = schemas.[schema_id]'
                                                    + ' LEFT OUTER JOIN sys.tables tables ON objects.[object_id] = tables.[object_id]'

          IF @PartitionLevelStatistics = 1
          BEGIN
            SET @CurrentCommand = @CurrentCommand + ' OUTER APPLY sys.dm_db_incremental_stats_properties(stats.object_id, stats.stats_id) dm_db_incremental_stats_properties'
          END

          SET @CurrentCommand = @CurrentCommand + ' WHERE objects.[type] IN(''U'',''V'')'
                                                    + CASE WHEN @Version >= 12 THEN ' AND (tables.is_memory_optimized = 0 OR tables.is_memory_optimized IS NULL)' ELSE '' END
                                                    + CASE WHEN @MSShippedObjects = 'N' THEN ' AND objects.is_ms_shipped = 0' ELSE '' END
                                                    + ' AND NOT EXISTS(SELECT * FROM sys.indexes indexes WHERE indexes.[object_id] = stats.[object_id] AND indexes.index_id = stats.stats_id)'
                                                    + ' AND NOT EXISTS(SELECT * FROM sys.indexes indexes2 WHERE indexes2.[object_id] = stats.[object_id] AND indexes2.type = 1 AND indexes2.is_disabled = 1)'

          IF @Version >= 12
          BEGIN
            SET @CurrentCommand = @CurrentCommand + ' UNION '

            SET @CurrentCommand = @CurrentCommand + 'SELECT schemas.[schema_id] AS SchemaID'
                                                      + ', schemas.[name] AS SchemaName'
                                                      + ', objects.[object_id] AS ObjectID'
                                                      + ', objects.[name] AS ObjectName'
                                                      + ', RTRIM(objects.[type]) AS ObjectType'
                                                      + ', tables.is_memory_optimized AS IsMemoryOptimized'
                                                      + ', NULL AS IndexID, NULL AS IndexName'
                                                      + ', NULL AS IndexType'
                                                      + ', NULL AS AllowPageLocks'
                                                      + ', NULL AS HasFilter'
                                                      + ', NULL AS IsImageText'
                                                      + ', NULL AS IsNewLOB'
                                                      + ', NULL AS IsFileStream'
                                                      + ', NULL AS HasClusteredColumnstore'
                                                      + ', NULL AS HasNonClusteredColumnstore'
                                                      + ', NULL AS IsComputed'
                                                      + ', NULL AS IsClusteredIndexComputed'
                                                      + ', NULL AS IsTimestamp'
                                                      + ', NULL AS OnReadOnlyFileGroup'
                                                      + ', NULL AS ResumableIndexOperation'
                                                      + ', stats.stats_id AS StatisticsID'
                                                      + ', stats.name AS StatisticsName'
                                                      + ', stats.no_recompute AS NoRecompute'
                                                      + ', ' + CASE WHEN @Version >= 12 THEN 'stats.is_incremental' ELSE '0' END + ' AS IsIncremental'
                                                      + ', NULL AS PartitionID'
                                                      + ', NULL AS PartitionNumber'
                                                      + ', NULL AS PartitionCount'
                                                      + ', 0 AS [Order]'
                                                      + ', 0 AS Selected'
                                                      + ', 0 AS Completed'
                                                      + ' FROM sys.stats stats'
                                                      + ' INNER JOIN sys.objects objects ON stats.[object_id] = objects.[object_id]'
                                                      + ' INNER JOIN sys.schemas schemas ON objects.[schema_id] = schemas.[schema_id]'
                                                      + ' INNER JOIN sys.tables tables ON objects.[object_id] = tables.[object_id]'

            SET @CurrentCommand = @CurrentCommand + ' WHERE objects.[type] = ''U'''
                                                      + ' AND tables.is_memory_optimized = 1'
                                                      + CASE WHEN @MSShippedObjects = 'N' THEN ' AND objects.is_ms_shipped = 0' ELSE '' END
                                                      + ' AND NOT EXISTS(SELECT * FROM sys.indexes indexes WHERE indexes.[object_id] = stats.[object_id] AND indexes.index_id = stats.stats_id)'
          END
        END

        SET @CurrentCommand = @CurrentCommand + ') IndexesStatistics'

        INSERT INTO @tmpIndexesStatistics (SchemaID, SchemaName, ObjectID, ObjectName, ObjectType, IsMemoryOptimized, IndexID, IndexName, IndexType, AllowPageLocks, HasFilter, IsImageText, IsNewLOB, IsFileStream, HasClusteredColumnstore, HasNonClusteredColumnstore, IsComputed, IsClusteredIndexComputed, IsTimestamp, OnReadOnlyFileGroup, ResumableIndexOperation, StatisticsID, StatisticsName, [NoRecompute], IsIncremental, PartitionID, PartitionNumber, PartitionCount, [Order], Selected, Completed)
        EXECUTE @CurrentDatabase_sp_executesql @stmt = @CurrentCommand
        SET @Error = @@ERROR
        IF @Error <> 0
        BEGIN
          SET @ReturnCode = @Error
        END
      END

      IF @Indexes IS NULL
      BEGIN
        UPDATE tmpIndexesStatistics
        SET tmpIndexesStatistics.Selected = 1
        FROM @tmpIndexesStatistics tmpIndexesStatistics
      END
      ELSE
      BEGIN
        UPDATE tmpIndexesStatistics
        SET tmpIndexesStatistics.Selected = SelectedIndexes.Selected
        FROM @tmpIndexesStatistics tmpIndexesStatistics
        INNER JOIN @SelectedIndexes SelectedIndexes
        ON @CurrentDatabaseName LIKE REPLACE(SelectedIndexes.DatabaseName,'_','[_]') AND tmpIndexesStatistics.SchemaName LIKE REPLACE(SelectedIndexes.SchemaName,'_','[_]') AND tmpIndexesStatistics.ObjectName LIKE REPLACE(SelectedIndexes.ObjectName,'_','[_]') AND COALESCE(tmpIndexesStatistics.IndexName,tmpIndexesStatistics.StatisticsName) LIKE REPLACE(SelectedIndexes.IndexName,'_','[_]')
        WHERE SelectedIndexes.Selected = 1

        UPDATE tmpIndexesStatistics
        SET tmpIndexesStatistics.Selected = SelectedIndexes.Selected
        FROM @tmpIndexesStatistics tmpIndexesStatistics
        INNER JOIN @SelectedIndexes SelectedIndexes
        ON @CurrentDatabaseName LIKE REPLACE(SelectedIndexes.DatabaseName,'_','[_]') AND tmpIndexesStatistics.SchemaName LIKE REPLACE(SelectedIndexes.SchemaName,'_','[_]') AND tmpIndexesStatistics.ObjectName LIKE REPLACE(SelectedIndexes.ObjectName,'_','[_]') AND COALESCE(tmpIndexesStatistics.IndexName,tmpIndexesStatistics.StatisticsName) LIKE REPLACE(SelectedIndexes.IndexName,'_','[_]')
        WHERE SelectedIndexes.Selected = 0

        UPDATE tmpIndexesStatistics
        SET tmpIndexesStatistics.StartPosition = SelectedIndexes2.StartPosition
        FROM @tmpIndexesStatistics tmpIndexesStatistics
        INNER JOIN (SELECT tmpIndexesStatistics.SchemaName, tmpIndexesStatistics.ObjectName, tmpIndexesStatistics.IndexName, tmpIndexesStatistics.StatisticsName, MIN(SelectedIndexes.StartPosition) AS StartPosition
                    FROM @tmpIndexesStatistics tmpIndexesStatistics
                    INNER JOIN @SelectedIndexes SelectedIndexes
                    ON @CurrentDatabaseName LIKE REPLACE(SelectedIndexes.DatabaseName,'_','[_]') AND tmpIndexesStatistics.SchemaName LIKE REPLACE(SelectedIndexes.SchemaName,'_','[_]') AND tmpIndexesStatistics.ObjectName LIKE REPLACE(SelectedIndexes.ObjectName,'_','[_]') AND COALESCE(tmpIndexesStatistics.IndexName,tmpIndexesStatistics.StatisticsName) LIKE REPLACE(SelectedIndexes.IndexName,'_','[_]')
                    WHERE SelectedIndexes.Selected = 1
                    GROUP BY tmpIndexesStatistics.SchemaName, tmpIndexesStatistics.ObjectName, tmpIndexesStatistics.IndexName, tmpIndexesStatistics.StatisticsName) SelectedIndexes2
        ON tmpIndexesStatistics.SchemaName = SelectedIndexes2.SchemaName
        AND tmpIndexesStatistics.ObjectName = SelectedIndexes2.ObjectName
        AND (tmpIndexesStatistics.IndexName = SelectedIndexes2.IndexName OR tmpIndexesStatistics.IndexName IS NULL)
        AND (tmpIndexesStatistics.StatisticsName = SelectedIndexes2.StatisticsName OR tmpIndexesStatistics.StatisticsName IS NULL)
      END;

      WITH tmpIndexesStatistics AS (
      SELECT SchemaName, ObjectName, [Order], ROW_NUMBER() OVER (ORDER BY ISNULL(ResumableIndexOperation,0) DESC, StartPosition ASC, SchemaName ASC, ObjectName ASC, CASE WHEN IndexType IS NULL THEN 1 ELSE 0 END ASC, IndexType ASC, IndexName ASC, StatisticsName ASC, PartitionNumber ASC) AS RowNumber
      FROM @tmpIndexesStatistics tmpIndexesStatistics
      WHERE Selected = 1
      )
      UPDATE tmpIndexesStatistics
      SET [Order] = RowNumber

      SET @ErrorMessage = ''
      SELECT @ErrorMessage = @ErrorMessage + QUOTENAME(DatabaseName) + '.' + QUOTENAME(SchemaName) + '.' + QUOTENAME(ObjectName) + ', '
      FROM @SelectedIndexes SelectedIndexes
      WHERE DatabaseName = @CurrentDatabaseName
      AND SchemaName NOT LIKE '%[%]%'
      AND ObjectName NOT LIKE '%[%]%'
      AND IndexName LIKE '%[%]%'
      AND NOT EXISTS (SELECT * FROM @tmpIndexesStatistics WHERE SchemaName = SelectedIndexes.SchemaName AND ObjectName = SelectedIndexes.ObjectName)
      IF @@ROWCOUNT > 0
      BEGIN
        SET @ErrorMessage = 'The following objects in the @Indexes parameter do not exist: ' + LEFT(@ErrorMessage,LEN(@ErrorMessage)-1) + '.'
        RAISERROR('%s',10,1,@ErrorMessage) WITH NOWAIT
        SET @Error = @@ERROR
        RAISERROR(@EmptyLine,10,1) WITH NOWAIT
      END

      SET @ErrorMessage = ''
      SELECT @ErrorMessage = @ErrorMessage + QUOTENAME(DatabaseName) + QUOTENAME(SchemaName) + '.' + QUOTENAME(ObjectName) + '.' + QUOTENAME(IndexName) + ', '
      FROM @SelectedIndexes SelectedIndexes
      WHERE DatabaseName = @CurrentDatabaseName
      AND SchemaName NOT LIKE '%[%]%'
      AND ObjectName NOT LIKE '%[%]%'
      AND IndexName NOT LIKE '%[%]%'
      AND NOT EXISTS (SELECT * FROM @tmpIndexesStatistics WHERE SchemaName = SelectedIndexes.SchemaName AND ObjectName = SelectedIndexes.ObjectName AND IndexName = SelectedIndexes.IndexName)
      IF @@ROWCOUNT > 0
      BEGIN
        SET @ErrorMessage = 'The following indexes in the @Indexes parameter do not exist: ' + LEFT(@ErrorMessage,LEN(@ErrorMessage)-1) + '.'
        RAISERROR('%s',10,1,@ErrorMessage) WITH NOWAIT
        SET @Error = @@ERROR
        RAISERROR(@EmptyLine,10,1) WITH NOWAIT
      END

      WHILE (SYSDATETIME() < DATEADD(SECOND,@TimeLimit,@StartTime) OR @TimeLimit IS NULL)
      BEGIN
        SELECT TOP 1 @CurrentIxID = ID,
                     @CurrentIxOrder = [Order],
                     @CurrentSchemaID = SchemaID,
                     @CurrentSchemaName = SchemaName,
                     @CurrentObjectID = ObjectID,
                     @CurrentObjectName = ObjectName,
                     @CurrentObjectType = ObjectType,
                     @CurrentIsMemoryOptimized = IsMemoryOptimized,
                     @CurrentIndexID = IndexID,
                     @CurrentIndexName = IndexName,
                     @CurrentIndexType = IndexType,
                     @CurrentAllowPageLocks = AllowPageLocks,
                     @CurrentHasFilter = HasFilter,
                     @CurrentIsImageText = IsImageText,
                     @CurrentIsNewLOB = IsNewLOB,
                     @CurrentIsFileStream = IsFileStream,
                     @CurrentHasClusteredColumnstore = HasClusteredColumnstore,
                     @CurrentHasNonClusteredColumnstore = HasNonClusteredColumnstore,
                     @CurrentIsComputed = IsComputed,
                     @CurrentIsClusteredIndexComputed = IsClusteredIndexComputed,
                     @CurrentIsTimestamp = IsTimestamp,
                     @CurrentOnReadOnlyFileGroup = OnReadOnlyFileGroup,
                     @CurrentResumableIndexOperation = ResumableIndexOperation,
                     @CurrentStatisticsID = StatisticsID,
                     @CurrentStatisticsName = StatisticsName,
                     @CurrentNoRecompute = [NoRecompute],
                     @CurrentIsIncremental = IsIncremental,
                     @CurrentPartitionID = PartitionID,
                     @CurrentPartitionNumber = PartitionNumber,
                     @CurrentPartitionCount = PartitionCount
        FROM @tmpIndexesStatistics
        WHERE Selected = 1
        AND Completed = 0
        ORDER BY [Order] ASC

        IF @@ROWCOUNT = 0
        BEGIN
          BREAK
        END

        -- Is the index a partition?
        IF @CurrentPartitionNumber IS NULL OR @CurrentPartitionCount = 1 BEGIN SET @CurrentIsPartition = 0 END ELSE BEGIN SET @CurrentIsPartition = 1 END

        -- Does the index exist?
        IF @CurrentIndexID IS NOT NULL AND EXISTS(SELECT * FROM @ActionsPreferred)
        BEGIN
          SET @CurrentCommand = ''

          IF @LockTimeout IS NOT NULL SET @CurrentCommand = 'SET LOCK_TIMEOUT ' + CAST(@LockTimeout * 1000 AS nvarchar) + '; '

          IF @CurrentIsPartition = 0 SET @CurrentCommand += 'IF EXISTS(SELECT * FROM sys.indexes indexes INNER JOIN sys.objects objects ON indexes.[object_id] = objects.[object_id] INNER JOIN sys.schemas schemas ON objects.[schema_id] = schemas.[schema_id] WHERE objects.[type] IN(''U'',''V'') AND indexes.[type] IN(1,2,3,4,5,6,7) AND indexes.is_disabled = 0 AND indexes.is_hypothetical = 0 AND schemas.[schema_id] = @ParamSchemaID AND schemas.[name] = @ParamSchemaName AND objects.[object_id] = @ParamObjectID AND objects.[name] = @ParamObjectName AND objects.[type] = @ParamObjectType AND indexes.index_id = @ParamIndexID AND indexes.[name] = @ParamIndexName AND indexes.[type] = @ParamIndexType) BEGIN SET @ParamIndexExists = 1 END'
          IF @CurrentIsPartition = 1 SET @CurrentCommand += 'IF EXISTS(SELECT * FROM sys.indexes indexes INNER JOIN sys.objects objects ON indexes.[object_id] = objects.[object_id] INNER JOIN sys.schemas schemas ON objects.[schema_id] = schemas.[schema_id] INNER JOIN sys.partitions partitions ON indexes.[object_id] = partitions.[object_id] AND indexes.index_id = partitions.index_id WHERE objects.[type] IN(''U'',''V'') AND indexes.[type] IN(1,2,3,4,5,6,7) AND indexes.is_disabled = 0 AND indexes.is_hypothetical = 0 AND schemas.[schema_id] = @ParamSchemaID AND schemas.[name] = @ParamSchemaName AND objects.[object_id] = @ParamObjectID AND objects.[name] = @ParamObjectName AND objects.[type] = @ParamObjectType AND indexes.index_id = @ParamIndexID AND indexes.[name] = @ParamIndexName AND indexes.[type] = @ParamIndexType AND partitions.partition_id = @ParamPartitionID AND partitions.partition_number = @ParamPartitionNumber) BEGIN SET @ParamIndexExists = 1 END'

          BEGIN TRY
            EXECUTE @CurrentDatabase_sp_executesql @stmt = @CurrentCommand, @params = N'@ParamSchemaID int, @ParamSchemaName sysname, @ParamObjectID int, @ParamObjectName sysname, @ParamObjectType sysname, @ParamIndexID int, @ParamIndexName sysname, @ParamIndexType int, @ParamPartitionID bigint, @ParamPartitionNumber int, @ParamIndexExists bit OUTPUT', @ParamSchemaID = @CurrentSchemaID, @ParamSchemaName = @CurrentSchemaName, @ParamObjectID = @CurrentObjectID, @ParamObjectName = @CurrentObjectName, @ParamObjectType = @CurrentObjectType, @ParamIndexID = @CurrentIndexID, @ParamIndexName = @CurrentIndexName, @ParamIndexType = @CurrentIndexType, @ParamPartitionID = @CurrentPartitionID, @ParamPartitionNumber = @CurrentPartitionNumber, @ParamIndexExists = @CurrentIndexExists OUTPUT

            IF @CurrentIndexExists IS NULL
            BEGIN
              SET @CurrentIndexExists = 0
              GOTO NoAction
            END
          END TRY
          BEGIN CATCH
            SET @ErrorMessage = 'Msg ' + CAST(ERROR_NUMBER() AS nvarchar) + ', ' + ISNULL(ERROR_MESSAGE(),'') + CASE WHEN ERROR_NUMBER() = 1222 THEN ' The index ' + QUOTENAME(@CurrentIndexName) + ' on the object ' + QUOTENAME(@CurrentDatabaseName) + '.' + QUOTENAME(@CurrentSchemaName) + '.' + QUOTENAME(@CurrentObjectName) + ' is locked. It could not be checked if the index exists.' ELSE '' END
            SET @Severity = CASE WHEN ERROR_NUMBER() IN(1205,1222) THEN @LockMessageSeverity ELSE 16 END
            RAISERROR('%s',@Severity,1,@ErrorMessage) WITH NOWAIT
            RAISERROR(@EmptyLine,10,1) WITH NOWAIT

            IF NOT (ERROR_NUMBER() IN(1205,1222) AND @LockMessageSeverity = 10)
            BEGIN
              SET @ReturnCode = ERROR_NUMBER()
            END

            GOTO NoAction
          END CATCH
        END

        -- Does the statistics exist?
        IF @CurrentStatisticsID IS NOT NULL AND @UpdateStatistics IS NOT NULL
        BEGIN
          SET @CurrentCommand = ''

          IF @LockTimeout IS NOT NULL SET @CurrentCommand = 'SET LOCK_TIMEOUT ' + CAST(@LockTimeout * 1000 AS nvarchar) + '; '

          SET @CurrentCommand += 'IF EXISTS(SELECT * FROM sys.stats stats INNER JOIN sys.objects objects ON stats.[object_id] = objects.[object_id] INNER JOIN sys.schemas schemas ON objects.[schema_id] = schemas.[schema_id] WHERE objects.[type] IN(''U'',''V'')' + CASE WHEN @MSShippedObjects = 'N' THEN ' AND objects.is_ms_shipped = 0' ELSE '' END + ' AND schemas.[schema_id] = @ParamSchemaID AND schemas.[name] = @ParamSchemaName AND objects.[object_id] = @ParamObjectID AND objects.[name] = @ParamObjectName AND objects.[type] = @ParamObjectType AND stats.stats_id = @ParamStatisticsID AND stats.[name] = @ParamStatisticsName) BEGIN SET @ParamStatisticsExists = 1 END'

          BEGIN TRY
            EXECUTE @CurrentDatabase_sp_executesql @stmt = @CurrentCommand, @params = N'@ParamSchemaID int, @ParamSchemaName sysname, @ParamObjectID int, @ParamObjectName sysname, @ParamObjectType sysname, @ParamStatisticsID int, @ParamStatisticsName sysname, @ParamStatisticsExists bit OUTPUT', @ParamSchemaID = @CurrentSchemaID, @ParamSchemaName = @CurrentSchemaName, @ParamObjectID = @CurrentObjectID, @ParamObjectName = @CurrentObjectName, @ParamObjectType = @CurrentObjectType, @ParamStatisticsID = @CurrentStatisticsID, @ParamStatisticsName = @CurrentStatisticsName, @ParamStatisticsExists = @CurrentStatisticsExists OUTPUT

            IF @CurrentStatisticsExists IS NULL
            BEGIN
              SET @CurrentStatisticsExists = 0
              GOTO NoAction
            END
          END TRY
          BEGIN CATCH
            SET @ErrorMessage = 'Msg ' + CAST(ERROR_NUMBER() AS nvarchar) + ', ' + ISNULL(ERROR_MESSAGE(),'') + CASE WHEN ERROR_NUMBER() = 1222 THEN ' The statistics ' + QUOTENAME(@CurrentStatisticsName) + ' on the object ' + QUOTENAME(@CurrentDatabaseName) + '.' + QUOTENAME(@CurrentSchemaName) + '.' + QUOTENAME(@CurrentObjectName) + ' is locked. It could not be checked if the statistics exists.' ELSE '' END
            SET @Severity = CASE WHEN ERROR_NUMBER() IN(1205,1222) THEN @LockMessageSeverity ELSE 16 END
            RAISERROR('%s',@Severity,1,@ErrorMessage) WITH NOWAIT
            RAISERROR(@EmptyLine,10,1) WITH NOWAIT

            IF NOT (ERROR_NUMBER() IN(1205,1222) AND @LockMessageSeverity = 10)
            BEGIN
              SET @ReturnCode = ERROR_NUMBER()
            END

            GOTO NoAction
          END CATCH
        END

        -- Has the data in the statistics been modified since the statistics was last updated?
        IF @CurrentStatisticsID IS NOT NULL AND @UpdateStatistics IS NOT NULL
        BEGIN
          SET @CurrentCommand = ''

          IF @LockTimeout IS NOT NULL SET @CurrentCommand = 'SET LOCK_TIMEOUT ' + CAST(@LockTimeout * 1000 AS nvarchar) + '; '

          IF @PartitionLevelStatistics = 1 AND @CurrentIsIncremental = 1
          BEGIN
            SET @CurrentCommand += 'SELECT @ParamRowCount = [rows], @ParamModificationCounter = modification_counter FROM sys.dm_db_incremental_stats_properties (@ParamObjectID, @ParamStatisticsID) WHERE partition_number = @ParamPartitionNumber'
          END
          ELSE
          IF (@Version >= 10.504000 AND @Version < 11) OR @Version >= 11.03000
          BEGIN
            SET @CurrentCommand += 'SELECT @ParamRowCount = [rows], @ParamModificationCounter = modification_counter FROM sys.dm_db_stats_properties (@ParamObjectID, @ParamStatisticsID)'
          END
          ELSE
          BEGIN
            SET @CurrentCommand += 'SELECT @ParamRowCount = rowcnt, @ParamModificationCounter = rowmodctr FROM sys.sysindexes sysindexes WHERE sysindexes.[id] = @ParamObjectID AND sysindexes.[indid] = @ParamStatisticsID'
          END

          BEGIN TRY
            EXECUTE @CurrentDatabase_sp_executesql @stmt = @CurrentCommand, @params = N'@ParamObjectID int, @ParamStatisticsID int, @ParamPartitionNumber int, @ParamRowCount bigint OUTPUT, @ParamModificationCounter bigint OUTPUT', @ParamObjectID = @CurrentObjectID, @ParamStatisticsID = @CurrentStatisticsID, @ParamPartitionNumber = @CurrentPartitionNumber, @ParamRowCount = @CurrentRowCount OUTPUT, @ParamModificationCounter = @CurrentModificationCounter OUTPUT

            IF @CurrentRowCount IS NULL SET @CurrentRowCount = 0
            IF @CurrentModificationCounter IS NULL SET @CurrentModificationCounter = 0
          END TRY
          BEGIN CATCH
            SET @ErrorMessage = 'Msg ' + CAST(ERROR_NUMBER() AS nvarchar) + ', ' + ISNULL(ERROR_MESSAGE(),'') + CASE WHEN ERROR_NUMBER() = 1222 THEN ' The statistics ' + QUOTENAME(@CurrentStatisticsName) + ' on the object ' + QUOTENAME(@CurrentDatabaseName) + '.' + QUOTENAME(@CurrentSchemaName) + '.' + QUOTENAME(@CurrentObjectName) + ' is locked. The rows and modification_counter could not be checked.' ELSE '' END
            SET @Severity = CASE WHEN ERROR_NUMBER() IN(1205,1222) THEN @LockMessageSeverity ELSE 16 END
            RAISERROR('%s',@Severity,1,@ErrorMessage) WITH NOWAIT
            RAISERROR(@EmptyLine,10,1) WITH NOWAIT

            IF NOT (ERROR_NUMBER() IN(1205,1222) AND @LockMessageSeverity = 10)
            BEGIN
              SET @ReturnCode = ERROR_NUMBER()
            END

            GOTO NoAction
          END CATCH
        END

        -- Is the index fragmented?
        IF @CurrentIndexID IS NOT NULL
        AND @CurrentOnReadOnlyFileGroup = 0
        AND EXISTS(SELECT * FROM @ActionsPreferred)
        AND (EXISTS(SELECT [Priority], [Action], COUNT(*) FROM @ActionsPreferred GROUP BY [Priority], [Action] HAVING COUNT(*) <> 3) OR @MinNumberOfPages > 0 OR @MaxNumberOfPages IS NOT NULL)
        AND NOT (SERVERPROPERTY('EngineEdition') = 8 AND @CurrentDatabaseName IN ('master', 'model'))
        BEGIN
          SET @CurrentCommand = ''

          IF @LockTimeout IS NOT NULL SET @CurrentCommand = 'SET LOCK_TIMEOUT ' + CAST(@LockTimeout * 1000 AS nvarchar) + '; '

          SET @CurrentCommand += 'SELECT @ParamFragmentationLevel = MAX(avg_fragmentation_in_percent), @ParamPageCount = SUM(page_count) FROM sys.dm_db_index_physical_stats(DB_ID(@ParamDatabaseName), @ParamObjectID, @ParamIndexID, @ParamPartitionNumber, ''LIMITED'') WHERE alloc_unit_type_desc = ''IN_ROW_DATA'' AND index_level = 0'

          BEGIN TRY
            EXECUTE sp_executesql @stmt = @CurrentCommand, @params = N'@ParamDatabaseName nvarchar(max), @ParamObjectID int, @ParamIndexID int, @ParamPartitionNumber int, @ParamFragmentationLevel float OUTPUT, @ParamPageCount bigint OUTPUT', @ParamDatabaseName = @CurrentDatabaseName, @ParamObjectID = @CurrentObjectID, @ParamIndexID = @CurrentIndexID, @ParamPartitionNumber = @CurrentPartitionNumber, @ParamFragmentationLevel = @CurrentFragmentationLevel OUTPUT, @ParamPageCount = @CurrentPageCount OUTPUT
          END TRY
          BEGIN CATCH
            SET @ErrorMessage = 'Msg ' + CAST(ERROR_NUMBER() AS nvarchar) + ', ' + ISNULL(ERROR_MESSAGE(),'') + CASE WHEN ERROR_NUMBER() = 1222 THEN ' The index ' + QUOTENAME(@CurrentIndexName) + ' on the object ' + QUOTENAME(@CurrentDatabaseName) + '.' + QUOTENAME(@CurrentSchemaName) + '.' + QUOTENAME(@CurrentObjectName) + ' is locked. The page_count and avg_fragmentation_in_percent could not be checked.' ELSE '' END
            SET @Severity = CASE WHEN ERROR_NUMBER() IN(1205,1222) THEN @LockMessageSeverity ELSE 16 END
            RAISERROR('%s',@Severity,1,@ErrorMessage) WITH NOWAIT
            RAISERROR(@EmptyLine,10,1) WITH NOWAIT

            IF NOT (ERROR_NUMBER() IN(1205,1222) AND @LockMessageSeverity = 10)
            BEGIN
              SET @ReturnCode = ERROR_NUMBER()
            END

            GOTO NoAction
          END CATCH
        END

        -- Select fragmentation group
        IF @CurrentIndexID IS NOT NULL AND @CurrentOnReadOnlyFileGroup = 0 AND EXISTS(SELECT * FROM @ActionsPreferred)
        BEGIN
          SET @CurrentFragmentationGroup = CASE
          WHEN @CurrentFragmentationLevel >= @FragmentationLevel2 THEN 'High'
          WHEN @CurrentFragmentationLevel >= @FragmentationLevel1 AND @CurrentFragmentationLevel < @FragmentationLevel2 THEN 'Medium'
          WHEN @CurrentFragmentationLevel < @FragmentationLevel1 THEN 'Low'
          END
        END

        -- Which actions are allowed?
        IF @CurrentIndexID IS NOT NULL AND EXISTS(SELECT * FROM @ActionsPreferred)
        BEGIN
          IF NOT (@CurrentOnReadOnlyFileGroup = 1)
          AND NOT (@CurrentIsMemoryOptimized = 1)
          AND NOT (@CurrentAllowPageLocks = 0)
          BEGIN
            INSERT INTO @CurrentActionsAllowed ([Action])
            VALUES ('INDEX_REORGANIZE')
          END
          IF NOT (@CurrentOnReadOnlyFileGroup = 1)
          AND NOT (@CurrentIsMemoryOptimized = 1)
          BEGIN
            INSERT INTO @CurrentActionsAllowed ([Action])
            VALUES ('INDEX_REBUILD_OFFLINE')
          END
          IF SERVERPROPERTY('EngineEdition') IN (3, 5, 8)
          AND NOT (@CurrentOnReadOnlyFileGroup = 1)
          AND NOT (@CurrentIsMemoryOptimized = 1)
          AND NOT (@CurrentIsPartition = 1 AND @Version < 12)
          AND NOT (@CurrentIndexType = 1 AND @CurrentIsImageText = 1)
          AND NOT (@CurrentIndexType = 1 AND @CurrentIsFileStream = 1)
          AND NOT (@CurrentIndexType = 1 AND @CurrentIsNewLOB = 1 AND @Version < 11)
          AND NOT (@CurrentIndexType = 2 AND @CurrentIsNewLOB = 1 AND @Version < 11)
          AND NOT (@CurrentIndexType = 3)
          AND NOT (@CurrentIndexType = 4)
          AND NOT (@CurrentIndexType = 5 AND @Version < 15)
          AND NOT (@CurrentIndexType = 6 AND @Version < 14)
          AND NOT (@CurrentIndexType = 1 AND @CurrentHasNonClusteredColumnstore = 1 AND @Version < 13)
          AND NOT (@CurrentIndexType = 2 AND @CurrentHasClusteredColumnstore = 1 AND @Version < 15)
          AND NOT (@CurrentIndexType = 2 AND @CurrentHasNonClusteredColumnstore = 1 AND @Version < 13)
          BEGIN
            INSERT INTO @CurrentActionsAllowed ([Action])
            VALUES ('INDEX_REBUILD_ONLINE')
          END
        END

        -- Decide action
        IF @CurrentIndexID IS NOT NULL
        AND EXISTS(SELECT * FROM @ActionsPreferred)
        AND (@CurrentPageCount >= @MinNumberOfPages OR @MinNumberOfPages = 0)
        AND (@CurrentPageCount <= @MaxNumberOfPages OR @MaxNumberOfPages IS NULL)
        AND @CurrentResumableIndexOperation = 0
        BEGIN
          IF EXISTS(SELECT [Priority], [Action], COUNT(*) FROM @ActionsPreferred GROUP BY [Priority], [Action] HAVING COUNT(*) <> 3)
          BEGIN
            SELECT @CurrentAction = [Action]
            FROM @ActionsPreferred
            WHERE FragmentationGroup = @CurrentFragmentationGroup
            AND [Priority] = (SELECT MIN([Priority])
                              FROM @ActionsPreferred
                              WHERE FragmentationGroup = @CurrentFragmentationGroup
                              AND [Action] IN (SELECT [Action] FROM @CurrentActionsAllowed))
          END
          ELSE
          BEGIN
            SELECT @CurrentAction = [Action]
            FROM @ActionsPreferred
            WHERE [Priority] = (SELECT MIN([Priority])
                                FROM @ActionsPreferred
                                WHERE [Action] IN (SELECT [Action] FROM @CurrentActionsAllowed))
          END
        END

        IF @CurrentResumableIndexOperation = 1
        BEGIN
          SET @CurrentAction = 'INDEX_REBUILD_ONLINE'
        END

        -- Workaround for limitation in SQL Server, http://support.microsoft.com/kb/2292737
        IF @CurrentIndexID IS NOT NULL
        BEGIN
          SET @CurrentMaxDOP = @MaxDOP

          IF @CurrentAction = 'INDEX_REBUILD_ONLINE' AND @CurrentAllowPageLocks = 0
          BEGIN
            SET @CurrentMaxDOP = 1
          END
        END

        -- Update statistics?
        IF @CurrentStatisticsID IS NOT NULL
        AND ((@UpdateStatistics = 'ALL' AND (@CurrentIndexType IN (1,2,3,4,7) OR @CurrentIndexID IS NULL)) OR (@UpdateStatistics = 'INDEX' AND @CurrentIndexID IS NOT NULL AND @CurrentIndexType IN (1,2,3,4,7)) OR (@UpdateStatistics = 'COLUMNS' AND @CurrentIndexID IS NULL))
        AND ((@OnlyModifiedStatistics = 'N' AND @StatisticsModificationLevel IS NULL) OR (@OnlyModifiedStatistics = 'Y' AND @CurrentModificationCounter > 0) OR ((@CurrentModificationCounter * 1. / NULLIF(@CurrentRowCount,0)) * 100 >= @StatisticsModificationLevel) OR (@StatisticsModificationLevel IS NOT NULL AND @CurrentModificationCounter > 0 AND (@CurrentModificationCounter >= SQRT(@CurrentRowCount * 1000))) OR (@CurrentIsMemoryOptimized = 1 AND NOT (@Version >= 13 OR SERVERPROPERTY('EngineEdition') IN (5,8))))
        AND ((@CurrentIsPartition = 0 AND (@CurrentAction NOT IN('INDEX_REBUILD_ONLINE','INDEX_REBUILD_OFFLINE') OR @CurrentAction IS NULL)) OR (@CurrentIsPartition = 1 AND (@CurrentPartitionNumber = @CurrentPartitionCount OR (@PartitionLevelStatistics = 1 AND @CurrentIsIncremental = 1))))
        BEGIN
          SET @CurrentUpdateStatistics = 'Y'
        END
        ELSE
        BEGIN
          SET @CurrentUpdateStatistics = 'N'
        END

        SET @CurrentStatisticsSample = @StatisticsSample
        SET @CurrentStatisticsResample = @StatisticsResample

        -- Memory-optimized tables only supports FULLSCAN and RESAMPLE in SQL Server 2014
        IF @CurrentIsMemoryOptimized = 1 AND NOT (@Version >= 13 OR SERVERPROPERTY('EngineEdition') IN (5,8)) AND (@CurrentStatisticsSample <> 100 OR @CurrentStatisticsSample IS NULL)
        BEGIN
          SET @CurrentStatisticsSample = NULL
          SET @CurrentStatisticsResample = 'Y'
        END

        -- Incremental statistics only supports RESAMPLE
        IF @PartitionLevelStatistics = 1 AND @CurrentIsIncremental = 1
        BEGIN
          SET @CurrentStatisticsSample = NULL
          SET @CurrentStatisticsResample = 'Y'
        END

        -- Create index comment
        IF @CurrentIndexID IS NOT NULL
        BEGIN
          SET @CurrentComment = 'ObjectType: ' + CASE WHEN @CurrentObjectType = 'U' THEN 'Table' WHEN @CurrentObjectType = 'V' THEN 'View' ELSE 'N/A' END + ', '
          SET @CurrentComment += 'IndexType: ' + CASE WHEN @CurrentIndexType = 1 THEN 'Clustered' WHEN @CurrentIndexType = 2 THEN 'NonClustered' WHEN @CurrentIndexType = 3 THEN 'XML' WHEN @CurrentIndexType = 4 THEN 'Spatial' WHEN @CurrentIndexType = 5 THEN 'Clustered Columnstore' WHEN @CurrentIndexType = 6 THEN 'NonClustered Columnstore' WHEN @CurrentIndexType = 7 THEN 'NonClustered Hash' ELSE 'N/A' END + ', '
          SET @CurrentComment += 'ImageText: ' + CASE WHEN @CurrentIsImageText = 1 THEN 'Yes' WHEN @CurrentIsImageText = 0 THEN 'No' ELSE 'N/A' END + ', '
          SET @CurrentComment += 'NewLOB: ' + CASE WHEN @CurrentIsNewLOB = 1 THEN 'Yes' WHEN @CurrentIsNewLOB = 0 THEN 'No' ELSE 'N/A' END + ', '
          SET @CurrentComment += 'FileStream: ' + CASE WHEN @CurrentIsFileStream = 1 THEN 'Yes' WHEN @CurrentIsFileStream = 0 THEN 'No' ELSE 'N/A' END + ', '
          IF @Version >= 12 AND @CurrentIndexType NOT IN(5, 6) SET @CurrentComment += 'HasClusteredColumnstore: ' + CASE WHEN @CurrentHasClusteredColumnstore = 1 THEN 'Yes' WHEN @CurrentHasClusteredColumnstore = 0 THEN 'No' ELSE 'N/A' END + ', '
          IF @Version >= 11 AND @CurrentIndexType NOT IN(5, 6) SET @CurrentComment += 'HasNonClusteredColumnstore: ' + CASE WHEN @CurrentHasNonClusteredColumnstore = 1 THEN 'Yes' WHEN @CurrentHasNonClusteredColumnstore = 0 THEN 'No' ELSE 'N/A' END + ', '
          IF @Version >= 14 AND @Resumable = 'Y' SET @CurrentComment += 'Computed: ' + CASE WHEN @CurrentIsComputed = 1 THEN 'Yes' WHEN @CurrentIsComputed = 0 THEN 'No' ELSE 'N/A' END + ', '
          IF @Version >= 14 AND @Resumable = 'Y' AND @CurrentIndexType = 2 SET @CurrentComment += 'ClusteredIndexComputed: ' + CASE WHEN @CurrentIsClusteredIndexComputed = 1 THEN 'Yes' WHEN @CurrentIsClusteredIndexComputed = 0 THEN 'No' ELSE 'N/A' END + ', '
          IF @Version >= 14 AND @Resumable = 'Y' SET @CurrentComment += 'Timestamp: ' + CASE WHEN @CurrentIsTimestamp = 1 THEN 'Yes' WHEN @CurrentIsTimestamp = 0 THEN 'No' ELSE 'N/A' END + ', '
          IF @Version >= 14 AND @Resumable = 'Y' SET @CurrentComment += 'HasFilter: ' + CASE WHEN @CurrentHasFilter = 1 THEN 'Yes' WHEN @CurrentHasFilter = 0 THEN 'No' ELSE 'N/A' END + ', '
          SET @CurrentComment += 'AllowPageLocks: ' + CASE WHEN @CurrentAllowPageLocks = 1 THEN 'Yes' WHEN @CurrentAllowPageLocks = 0 THEN 'No' ELSE 'N/A' END + ', '
          SET @CurrentComment += 'PageCount: ' + ISNULL(CAST(@CurrentPageCount AS nvarchar),'N/A') + ', '
          SET @CurrentComment += 'Fragmentation: ' + ISNULL(CAST(@CurrentFragmentationLevel AS nvarchar),'N/A')
        END

        IF @CurrentIndexID IS NOT NULL AND (@CurrentPageCount IS NOT NULL OR @CurrentFragmentationLevel IS NOT NULL)
        BEGIN
        SET @CurrentExtendedInfo = (SELECT *
                                    FROM (SELECT CAST(@CurrentPageCount AS nvarchar) AS [PageCount],
                                                 CAST(@CurrentFragmentationLevel AS nvarchar) AS Fragmentation
                                    ) ExtendedInfo FOR XML RAW('ExtendedInfo'), ELEMENTS)
        END

        IF @CurrentIndexID IS NOT NULL AND @CurrentAction IS NOT NULL AND (SYSDATETIME() < DATEADD(SECOND,@TimeLimit,@StartTime) OR @TimeLimit IS NULL)
        BEGIN
          SET @CurrentDatabaseContext = @CurrentDatabaseName

          SET @CurrentCommandType = 'ALTER_INDEX'

          SET @CurrentCommand = ''
          IF @LockTimeout IS NOT NULL SET @CurrentCommand = 'SET LOCK_TIMEOUT ' + CAST(@LockTimeout * 1000 AS nvarchar) + '; '
          SET @CurrentCommand += 'ALTER INDEX ' + QUOTENAME(@CurrentIndexName) + ' ON ' + QUOTENAME(@CurrentSchemaName) + '.' + QUOTENAME(@CurrentObjectName)
          IF @CurrentResumableIndexOperation = 1 SET @CurrentCommand += ' RESUME'
          IF @CurrentAction IN('INDEX_REBUILD_ONLINE','INDEX_REBUILD_OFFLINE') AND @CurrentResumableIndexOperation = 0 SET @CurrentCommand += ' REBUILD'
          IF @CurrentAction IN('INDEX_REORGANIZE') AND @CurrentResumableIndexOperation = 0 SET @CurrentCommand += ' REORGANIZE'
          IF @CurrentIsPartition = 1 AND @CurrentResumableIndexOperation = 0 SET @CurrentCommand += ' PARTITION = ' + CAST(@CurrentPartitionNumber AS nvarchar)

          IF @CurrentAction IN('INDEX_REBUILD_ONLINE','INDEX_REBUILD_OFFLINE') AND @SortInTempdb = 'Y' AND @CurrentIndexType IN(1,2,3,4) AND @CurrentResumableIndexOperation = 0
          BEGIN
            INSERT INTO @CurrentAlterIndexWithClauseArguments (Argument)
            SELECT 'SORT_IN_TEMPDB = ON'
          END

          IF @CurrentAction IN('INDEX_REBUILD_ONLINE','INDEX_REBUILD_OFFLINE') AND @SortInTempdb = 'N' AND @CurrentIndexType IN(1,2,3,4) AND @CurrentResumableIndexOperation = 0
          BEGIN
            INSERT INTO @CurrentAlterIndexWithClauseArguments (Argument)
            SELECT 'SORT_IN_TEMPDB = OFF'
          END

          IF @CurrentAction = 'INDEX_REBUILD_ONLINE' AND (@CurrentIsPartition = 0 OR @Version >= 12) AND @CurrentResumableIndexOperation = 0
          BEGIN
            INSERT INTO @CurrentAlterIndexWithClauseArguments (Argument)
            SELECT 'ONLINE = ON' + CASE WHEN @WaitAtLowPriorityMaxDuration IS NOT NULL THEN ' (WAIT_AT_LOW_PRIORITY (MAX_DURATION = ' + CAST(@WaitAtLowPriorityMaxDuration AS nvarchar) + ', ABORT_AFTER_WAIT = ' + UPPER(@WaitAtLowPriorityAbortAfterWait) + '))' ELSE '' END
          END

          IF @CurrentAction = 'INDEX_REBUILD_OFFLINE' AND (@CurrentIsPartition = 0 OR @Version >= 12) AND @CurrentResumableIndexOperation = 0
          BEGIN
            INSERT INTO @CurrentAlterIndexWithClauseArguments (Argument)
            SELECT 'ONLINE = OFF'
          END

          IF @CurrentAction IN('INDEX_REBUILD_ONLINE','INDEX_REBUILD_OFFLINE') AND @CurrentMaxDOP IS NOT NULL
          BEGIN
            INSERT INTO @CurrentAlterIndexWithClauseArguments (Argument)
            SELECT 'MAXDOP = ' + CAST(@CurrentMaxDOP AS nvarchar)
          END

          IF @CurrentAction IN('INDEX_REBUILD_ONLINE','INDEX_REBUILD_OFFLINE') AND @FillFactor IS NOT NULL AND @CurrentIsPartition = 0 AND @CurrentIndexType IN(1,2,3,4) AND @CurrentResumableIndexOperation = 0
          BEGIN
            INSERT INTO @CurrentAlterIndexWithClauseArguments (Argument)
            SELECT 'FILLFACTOR = ' + CAST(@FillFactor AS nvarchar)
          END

          IF @CurrentAction IN('INDEX_REBUILD_ONLINE','INDEX_REBUILD_OFFLINE') AND @PadIndex = 'Y' AND @CurrentIsPartition = 0 AND @CurrentIndexType IN(1,2,3,4) AND @CurrentResumableIndexOperation = 0
          BEGIN
            INSERT INTO @CurrentAlterIndexWithClauseArguments (Argument)
            SELECT 'PAD_INDEX = ON'
          END

          IF (@Version >= 14 OR SERVERPROPERTY('EngineEdition') IN (5,8)) AND @CurrentAction = 'INDEX_REBUILD_ONLINE' AND @CurrentResumableIndexOperation = 0
          BEGIN
            INSERT INTO @CurrentAlterIndexWithClauseArguments (Argument)
            SELECT CASE WHEN @Resumable = 'Y' AND @CurrentIndexType IN(1,2) AND @CurrentIsComputed = 0 AND @CurrentIsClusteredIndexComputed = 0 AND @CurrentIsTimestamp = 0 AND @CurrentHasFilter = 0 THEN 'RESUMABLE = ON' ELSE 'RESUMABLE = OFF' END
          END

          IF (@Version >= 14 OR SERVERPROPERTY('EngineEdition') IN (5,8)) AND @CurrentAction = 'INDEX_REBUILD_ONLINE' AND @Resumable = 'Y'  AND @CurrentIndexType IN(1,2) AND @CurrentIsComputed = 0 AND @CurrentIsClusteredIndexComputed = 0 AND @CurrentIsTimestamp = 0 AND @CurrentHasFilter = 0 AND @TimeLimit IS NOT NULL
          BEGIN
            INSERT INTO @CurrentAlterIndexWithClauseArguments (Argument)
            SELECT 'MAX_DURATION = ' + CAST(DATEDIFF(MINUTE,SYSDATETIME(),DATEADD(SECOND,@TimeLimit,@StartTime)) AS nvarchar(max))
          END

          IF @CurrentAction IN('INDEX_REORGANIZE') AND @LOBCompaction = 'Y'
          BEGIN
            INSERT INTO @CurrentAlterIndexWithClauseArguments (Argument)
            SELECT 'LOB_COMPACTION = ON'
          END

          IF @CurrentAction IN('INDEX_REORGANIZE') AND @LOBCompaction = 'N'
          BEGIN
            INSERT INTO @CurrentAlterIndexWithClauseArguments (Argument)
            SELECT 'LOB_COMPACTION = OFF'
          END

          IF EXISTS (SELECT * FROM @CurrentAlterIndexWithClauseArguments)
          BEGIN
            SET @CurrentAlterIndexWithClause = ' WITH ('

            WHILE (1 = 1)
            BEGIN
              SELECT TOP 1 @CurrentAlterIndexArgumentID = ID,
                           @CurrentAlterIndexArgument = Argument
              FROM @CurrentAlterIndexWithClauseArguments
              WHERE Added = 0
              ORDER BY ID ASC

              IF @@ROWCOUNT = 0
              BEGIN
                BREAK
              END

              SET @CurrentAlterIndexWithClause += @CurrentAlterIndexArgument + ', '

              UPDATE @CurrentAlterIndexWithClauseArguments
              SET Added = 1
              WHERE [ID] = @CurrentAlterIndexArgumentID
            END

            SET @CurrentAlterIndexWithClause = RTRIM(@CurrentAlterIndexWithClause)

            SET @CurrentAlterIndexWithClause = LEFT(@CurrentAlterIndexWithClause,LEN(@CurrentAlterIndexWithClause) - 1)

            SET @CurrentAlterIndexWithClause = @CurrentAlterIndexWithClause + ')'
          END

          IF @CurrentAlterIndexWithClause IS NOT NULL SET @CurrentCommand += @CurrentAlterIndexWithClause

          EXECUTE @CurrentCommandOutput = dbo.CommandExecute @DatabaseContext = @CurrentDatabaseName, @Command = @CurrentCommand, @CommandType = @CurrentCommandType, @Mode = 2, @Comment = @CurrentComment, @DatabaseName = @CurrentDatabaseName, @SchemaName = @CurrentSchemaName, @ObjectName = @CurrentObjectName, @ObjectType = @CurrentObjectType, @IndexName = @CurrentIndexName, @IndexType = @CurrentIndexType, @PartitionNumber = @CurrentPartitionNumber, @ExtendedInfo = @CurrentExtendedInfo, @LockMessageSeverity = @LockMessageSeverity, @ExecuteAsUser = @ExecuteAsUser, @LogToTable = @LogToTable, @Execute = @Execute
          SET @Error = @@ERROR
          IF @Error <> 0 SET @CurrentCommandOutput = @Error
          IF @CurrentCommandOutput <> 0 SET @ReturnCode = @CurrentCommandOutput

          IF @Delay > 0
          BEGIN
            SET @CurrentDelay = DATEADD(ss,@Delay,'1900-01-01')
            WAITFOR DELAY @CurrentDelay
          END
        END

        SET @CurrentMaxDOP = @MaxDOP

        -- Create statistics comment
        IF @CurrentStatisticsID IS NOT NULL
        BEGIN
          SET @CurrentComment = 'ObjectType: ' + CASE WHEN @CurrentObjectType = 'U' THEN 'Table' WHEN @CurrentObjectType = 'V' THEN 'View' ELSE 'N/A' END + ', '
          SET @CurrentComment += 'IndexType: ' + CASE WHEN @CurrentIndexID IS NOT NULL THEN 'Index' ELSE 'Column' END + ', '
          IF @CurrentIndexID IS NOT NULL SET @CurrentComment += 'IndexType: ' + CASE WHEN @CurrentIndexType = 1 THEN 'Clustered' WHEN @CurrentIndexType = 2 THEN 'NonClustered' WHEN @CurrentIndexType = 3 THEN 'XML' WHEN @CurrentIndexType = 4 THEN 'Spatial' WHEN @CurrentIndexType = 5 THEN 'Clustered Columnstore' WHEN @CurrentIndexType = 6 THEN 'NonClustered Columnstore' WHEN @CurrentIndexType = 7 THEN 'NonClustered Hash' ELSE 'N/A' END + ', '
          SET @CurrentComment += 'Incremental: ' + CASE WHEN @CurrentIsIncremental = 1 THEN 'Y' WHEN @CurrentIsIncremental = 0 THEN 'N' ELSE 'N/A' END + ', '
          SET @CurrentComment += 'RowCount: ' + ISNULL(CAST(@CurrentRowCount AS nvarchar),'N/A') + ', '
          SET @CurrentComment += 'ModificationCounter: ' + ISNULL(CAST(@CurrentModificationCounter AS nvarchar),'N/A')
        END

        IF @CurrentStatisticsID IS NOT NULL AND (@CurrentRowCount IS NOT NULL OR @CurrentModificationCounter IS NOT NULL)
        BEGIN
        SET @CurrentExtendedInfo = (SELECT *
                                    FROM (SELECT CAST(@CurrentRowCount AS nvarchar) AS [RowCount],
                                                 CAST(@CurrentModificationCounter AS nvarchar) AS ModificationCounter
                                    ) ExtendedInfo FOR XML RAW('ExtendedInfo'), ELEMENTS)
        END

        IF @CurrentStatisticsID IS NOT NULL AND @CurrentUpdateStatistics = 'Y' AND (SYSDATETIME() < DATEADD(SECOND,@TimeLimit,@StartTime) OR @TimeLimit IS NULL)
        BEGIN
          SET @CurrentDatabaseContext = @CurrentDatabaseName

          SET @CurrentCommandType = 'UPDATE_STATISTICS'

          SET @CurrentCommand = ''
          IF @LockTimeout IS NOT NULL SET @CurrentCommand = 'SET LOCK_TIMEOUT ' + CAST(@LockTimeout * 1000 AS nvarchar) + '; '
          SET @CurrentCommand += 'UPDATE STATISTICS ' + QUOTENAME(@CurrentSchemaName) + '.' + QUOTENAME(@CurrentObjectName) + ' ' + QUOTENAME(@CurrentStatisticsName)

          IF @CurrentMaxDOP IS NOT NULL AND ((@Version >= 12.06024 AND @Version < 13) OR (@Version >= 13.05026 AND @Version < 14) OR @Version >= 14.030154)
          BEGIN
            INSERT INTO @CurrentUpdateStatisticsWithClauseArguments (Argument)
            SELECT 'MAXDOP = ' + CAST(@CurrentMaxDOP AS nvarchar)
          END

          IF @CurrentStatisticsSample = 100
          BEGIN
            INSERT INTO @CurrentUpdateStatisticsWithClauseArguments (Argument)
            SELECT 'FULLSCAN'
          END

          IF @CurrentStatisticsSample IS NOT NULL AND @CurrentStatisticsSample <> 100
          BEGIN
            INSERT INTO @CurrentUpdateStatisticsWithClauseArguments (Argument)
            SELECT 'SAMPLE ' + CAST(@CurrentStatisticsSample AS nvarchar) + ' PERCENT'
          END

          IF @CurrentNoRecompute = 1
          BEGIN
            INSERT INTO @CurrentUpdateStatisticsWithClauseArguments (Argument)
            SELECT 'NORECOMPUTE'
          END

          IF @CurrentStatisticsResample = 'Y'
          BEGIN
            INSERT INTO @CurrentUpdateStatisticsWithClauseArguments (Argument)
            SELECT 'RESAMPLE'
          END

          IF EXISTS (SELECT * FROM @CurrentUpdateStatisticsWithClauseArguments)
          BEGIN
            SET @CurrentUpdateStatisticsWithClause = ' WITH'

            WHILE (1 = 1)
            BEGIN
              SELECT TOP 1 @CurrentUpdateStatisticsArgumentID = ID,
                           @CurrentUpdateStatisticsArgument = Argument
              FROM @CurrentUpdateStatisticsWithClauseArguments
              WHERE Added = 0
              ORDER BY ID ASC

              IF @@ROWCOUNT = 0
              BEGIN
                BREAK
              END

              SET @CurrentUpdateStatisticsWithClause = @CurrentUpdateStatisticsWithClause + ' ' + @CurrentUpdateStatisticsArgument + ','

              UPDATE @CurrentUpdateStatisticsWithClauseArguments
              SET Added = 1
              WHERE [ID] = @CurrentUpdateStatisticsArgumentID
            END

            SET @CurrentUpdateStatisticsWithClause = LEFT(@CurrentUpdateStatisticsWithClause,LEN(@CurrentUpdateStatisticsWithClause) - 1)
          END

          IF @CurrentUpdateStatisticsWithClause IS NOT NULL SET @CurrentCommand += @CurrentUpdateStatisticsWithClause

          IF @PartitionLevelStatistics = 1 AND @CurrentIsIncremental = 1 AND @CurrentPartitionNumber IS NOT NULL SET @CurrentCommand += ' ON PARTITIONS(' + CAST(@CurrentPartitionNumber AS nvarchar(max)) + ')'

          EXECUTE @CurrentCommandOutput = dbo.CommandExecute @DatabaseContext = @CurrentDatabaseName, @Command = @CurrentCommand, @CommandType = @CurrentCommandType, @Mode = 2, @Comment = @CurrentComment, @DatabaseName = @CurrentDatabaseName, @SchemaName = @CurrentSchemaName, @ObjectName = @CurrentObjectName, @ObjectType = @CurrentObjectType, @IndexName = @CurrentIndexName, @IndexType = @CurrentIndexType, @StatisticsName = @CurrentStatisticsName, @ExtendedInfo = @CurrentExtendedInfo, @LockMessageSeverity = @LockMessageSeverity, @ExecuteAsUser = @ExecuteAsUser, @LogToTable = @LogToTable, @Execute = @Execute
          SET @Error = @@ERROR
          IF @Error <> 0 SET @CurrentCommandOutput = @Error
          IF @CurrentCommandOutput <> 0 SET @ReturnCode = @CurrentCommandOutput
        END

        NoAction:

        -- Update that the index or statistics is completed
        UPDATE @tmpIndexesStatistics
        SET Completed = 1
        WHERE Selected = 1
        AND Completed = 0
        AND [Order] = @CurrentIxOrder
        AND ID = @CurrentIxID

        -- Clear variables
        SET @CurrentDatabaseContext = NULL

        SET @CurrentCommand = NULL
        SET @CurrentCommandOutput = NULL
        SET @CurrentCommandType = NULL
        SET @CurrentComment = NULL
        SET @CurrentExtendedInfo = NULL

        SET @CurrentIxID = NULL
        SET @CurrentIxOrder = NULL
        SET @CurrentSchemaID = NULL
        SET @CurrentSchemaName = NULL
        SET @CurrentObjectID = NULL
        SET @CurrentObjectName = NULL
        SET @CurrentObjectType = NULL
        SET @CurrentIsMemoryOptimized = NULL
        SET @CurrentIndexID = NULL
        SET @CurrentIndexName = NULL
        SET @CurrentIndexType = NULL
        SET @CurrentStatisticsID = NULL
        SET @CurrentStatisticsName = NULL
        SET @CurrentPartitionID = NULL
        SET @CurrentPartitionNumber = NULL
        SET @CurrentPartitionCount = NULL
        SET @CurrentIsPartition = NULL
        SET @CurrentIndexExists = NULL
        SET @CurrentStatisticsExists = NULL
        SET @CurrentIsImageText = NULL
        SET @CurrentIsNewLOB = NULL
        SET @CurrentIsFileStream = NULL
        SET @CurrentHasClusteredColumnstore = NULL
        SET @CurrentHasNonClusteredColumnstore = NULL
        SET @CurrentIsComputed = NULL
        SET @CurrentIsClusteredIndexComputed = NULL
        SET @CurrentIsTimestamp = NULL
        SET @CurrentAllowPageLocks = NULL
        SET @CurrentHasFilter = NULL
        SET @CurrentNoRecompute = NULL
        SET @CurrentIsIncremental = NULL
        SET @CurrentRowCount = NULL
        SET @CurrentModificationCounter = NULL
        SET @CurrentOnReadOnlyFileGroup = NULL
        SET @CurrentResumableIndexOperation = NULL
        SET @CurrentFragmentationLevel = NULL
        SET @CurrentPageCount = NULL
        SET @CurrentFragmentationGroup = NULL
        SET @CurrentAction = NULL
        SET @CurrentMaxDOP = NULL
        SET @CurrentUpdateStatistics = NULL
        SET @CurrentStatisticsSample = NULL
        SET @CurrentStatisticsResample = NULL
        SET @CurrentAlterIndexArgumentID = NULL
        SET @CurrentAlterIndexArgument = NULL
        SET @CurrentAlterIndexWithClause = NULL
        SET @CurrentUpdateStatisticsArgumentID = NULL
        SET @CurrentUpdateStatisticsArgument = NULL
        SET @CurrentUpdateStatisticsWithClause = NULL

        DELETE FROM @CurrentActionsAllowed
        DELETE FROM @CurrentAlterIndexWithClauseArguments
        DELETE FROM @CurrentUpdateStatisticsWithClauseArguments

      END

    END

    IF @CurrentDatabaseState = 'SUSPECT'
    BEGIN
      SET @ErrorMessage = 'The database ' + QUOTENAME(@CurrentDatabaseName) + ' is in a SUSPECT state.'
      RAISERROR('%s',16,1,@ErrorMessage) WITH NOWAIT
      RAISERROR(@EmptyLine,10,1) WITH NOWAIT
      SET @Error = @@ERROR
    END

    -- Update that the database is completed
    IF @DatabasesInParallel = 'Y'
    BEGIN
      UPDATE dbo.QueueDatabase
      SET DatabaseEndTime = SYSDATETIME()
      WHERE QueueID = @QueueID
      AND DatabaseName = @CurrentDatabaseName
    END
    ELSE
    BEGIN
      UPDATE @tmpDatabases
      SET Completed = 1
      WHERE Selected = 1
      AND Completed = 0
      AND ID = @CurrentDBID
    END

    -- Clear variables
    SET @CurrentDBID = NULL
    SET @CurrentDatabaseName = NULL

    SET @CurrentDatabase_sp_executesql = NULL

    SET @CurrentExecuteAsUserExists = NULL
    SET @CurrentUserAccess = NULL
    SET @CurrentIsReadOnly = NULL
    SET @CurrentDatabaseState = NULL
    SET @CurrentInStandby = NULL
    SET @CurrentRecoveryModel = NULL

    SET @CurrentReplicaID = NULL
    SET @CurrentAvailabilityGroupID = NULL
    SET @CurrentAvailabilityGroup = NULL
    SET @CurrentAvailabilityGroupRole = NULL
    SET @CurrentDatabaseMirroringRole = NULL

    SET @CurrentCommand = NULL

    DELETE FROM @tmpIndexesStatistics

  END

  ----------------------------------------------------------------------------------------------------
  --// Log completing information                                                                 //--
  ----------------------------------------------------------------------------------------------------

  Logging:
  SET @EndMessage = 'Date and time: ' + CONVERT(nvarchar,SYSDATETIME(),120)
  RAISERROR('%s',10,1,@EndMessage) WITH NOWAIT

  RAISERROR(@EmptyLine,10,1) WITH NOWAIT

  IF @ReturnCode <> 0
  BEGIN
    RETURN @ReturnCode
  END

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

END


GO

-- [dbo].[usp_ArchiveServerHealth]

-- Create stored procedure to move old data to archive
CREATE   PROCEDURE dbo.usp_ArchiveServerHealth
    @DaysToKeep INT = 90
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @CutoffDate DATETIME2 = DATEADD(DAY, -@DaysToKeep, SYSDATETIME());
    DECLARE @RowsArchived INT = 0;
    
    BEGIN TRANSACTION;
    
    BEGIN TRY
        -- Insert old records into archive
        INSERT INTO dbo.FACT_ServerHealth_Archive (
            CaptureTime, ServerName, CPU_Utilization, CPU_SignalWaitPct, 
            RunnableTasksCount, Scheduler_Total_CPU_Usage_MS, Memory_PLE, 
            Memory_GrantsPending, Memory_TotalMB, Memory_UsedMB, SQL_Memory_Model, 
            SQL_Memory_Model_Desc, Process_Physical_Memory_Low, Process_Virtual_Memory_Low,
            TempDB_UsageMB, TempDB_VersionStoreMB, ActiveSessions, BlockingSessions,
            LongRunningRequests, ErrorLog_CriticalCount, InstanceVersion, Edition,
            UptimeHours, BackupAge_Full_Hours, BackupAge_Diff_Hours, BackupAge_Log_Hours
        )
        SELECT 
            CaptureTime, ServerName, CPU_Utilization, CPU_SignalWaitPct, 
            RunnableTasksCount, Scheduler_Total_CPU_Usage_MS, Memory_PLE, 
            Memory_GrantsPending, Memory_TotalMB, Memory_UsedMB, SQL_Memory_Model, 
            SQL_Memory_Model_Desc, Process_Physical_Memory_Low, Process_Virtual_Memory_Low,
            TempDB_UsageMB, TempDB_VersionStoreMB, ActiveSessions, BlockingSessions,
            LongRunningRequests, ErrorLog_CriticalCount, InstanceVersion, Edition,
            UptimeHours, BackupAge_Full_Hours, BackupAge_Diff_Hours, BackupAge_Log_Hours
        FROM dbo.FACT_ServerHealth
        WHERE CaptureTime < @CutoffDate;
        
        SET @RowsArchived = @@ROWCOUNT;
        
        -- Delete archived records from main table
        DELETE FROM dbo.FACT_ServerHealth
        WHERE CaptureTime < @CutoffDate;
        
        COMMIT TRANSACTION;
        
        PRINT 'Successfully archived ' + CAST(@RowsArchived AS VARCHAR(20)) + ' records older than ' + CONVERT(VARCHAR(20), @CutoffDate, 120);
    END TRY
    BEGIN CATCH
        ROLLBACK TRANSACTION;
        THROW;
    END CATCH
END;

GO

-- [dbo].[usp_Check_AGFailover]
CREATE OR ALTER PROCEDURE dbo.usp_Check_AGFailover
AS
BEGIN
    SET NOCOUNT ON;

    IF EXISTS (
        SELECT * FROM sys.dm_hadr_availability_replica_states 
        WHERE role_desc = 'PRIMARY'
          AND last_connect_error_timestamp IS NOT NULL
    )
    BEGIN
        INSERT INTO dbo.fact_Alerts (ServerName, AlertType, AlertSeverity, AlertMessage)
        VALUES (
            @@SERVERNAME,
            'AG Failover',
            'HIGH',
            'Availability Group failover or role change detected'
        );
    END
END

GO

-- [dbo].[usp_Check_Blocking]
CREATE OR ALTER PROCEDURE dbo.usp_Check_Blocking
AS
BEGIN
    SET NOCOUNT ON;

    INSERT INTO dbo.fact_Alerts (ServerName, AlertType, AlertSeverity, AlertMessage)
    SELECT
        @@SERVERNAME,
        'Blocking Sessions',
        'HIGH',
        CONCAT('Blocking detected: Session ', blocking_session_id, 
               ' is blocking session ', session_id)
    FROM sys.dm_exec_requests
    WHERE blocking_session_id <> 0
      AND NOT EXISTS (
            SELECT 1 FROM dbo.fact_Alerts
            WHERE AlertType = 'Blocking Sessions'
              AND Processed = 0
              AND ServerName = @@SERVERNAME
        );
END

GO

-- [dbo].[usp_Check_CPUPressure]
CREATE OR ALTER PROCEDURE dbo.usp_Check_CPUPressure
AS
BEGIN
    SET NOCOUNT ON;

    INSERT INTO dbo.fact_Alerts (ServerName, AlertType, AlertSeverity, AlertMessage)
    SELECT
        ServerName,
        'High CPU',
        CASE WHEN SqlCpuPercent >= 90 THEN 'HIGH' ELSE 'MEDIUM' END,
        CONCAT('High CPU detected on ', ServerName, ': ', SqlCpuPercent, '% SQL CPU')
    FROM dbo.fact_PerformanceSnapshot
    WHERE SnapshotTime >= DATEADD(MINUTE, -5, GETDATE())
      AND SqlCpuPercent >= 80
      AND NOT EXISTS (
            SELECT 1 FROM dbo.fact_Alerts
            WHERE AlertType = 'High CPU'
              AND Processed = 0
              AND ServerName = fact_PerformanceSnapshot.ServerName
        );
END

GO

-- [dbo].[usp_Check_Deadlocks]
CREATE OR ALTER PROCEDURE dbo.usp_Check_Deadlocks
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @startTime DATETIME =
        (SELECT sqlserver_start_time FROM sys.dm_os_sys_info);

    INSERT INTO dbo.fact_Alerts (ServerName, AlertType, AlertSeverity, AlertMessage)
    SELECT
        @@SERVERNAME,
        'Deadlock',
        'HIGH',
        'Deadlock detected on server'
    FROM sys.dm_os_ring_buffers
    WHERE ring_buffer_type = 'RING_BUFFER_DEADLOCK'
      AND DATEADD(MILLISECOND, timestamp/1000, @startTime)
            > ISNULL((SELECT MAX(AlertTime)
                      FROM dbo.fact_Alerts
                      WHERE AlertType='Deadlock'), '2000-01-01')
      AND NOT EXISTS (
            SELECT 1 FROM dbo.fact_Alerts
            WHERE AlertType='Deadlock'
              AND Processed = 0
              AND ServerName = @@SERVERNAME
        );
END

GO

-- [dbo].[usp_Check_DiskSpace]
CREATE OR ALTER PROCEDURE dbo.usp_Check_DiskSpace
AS
BEGIN
    SET NOCOUNT ON;

    INSERT INTO dbo.fact_Alerts (ServerName, AlertType, AlertSeverity, AlertMessage)
    SELECT
        ServerName,
        'Low Disk Space',
        'HIGH',
        CONCAT('Low disk free space on ', ServerName, ' â€“ Drive ', DriveLetter, ' at ', FreePct, '%')
    FROM dbo.fact_DiskSpace
    WHERE FreePct < 10
      AND SnapshotTime >= DATEADD(MINUTE, -30, GETDATE())
      AND NOT EXISTS (
            SELECT 1 FROM dbo.fact_Alerts
            WHERE AlertType = 'Low Disk Space'
              AND Processed = 0
              AND ServerName = fact_DiskSpace.ServerName
        );
END

GO

-- [dbo].[usp_Check_Failover]

CREATE   PROCEDURE dbo.usp_Check_Failover
AS
BEGIN
    SET NOCOUNT ON;

    INSERT INTO dbo.fact_Alerts (ServerName, AlertType, AlertSeverity, AlertMessage, Processed, CaptureTime)
    SELECT 
        ServerName,
        'Failover Event',
        'HIGH',
        CONCAT(
            'Failover detected on ', ServerName, 
            '. New Role: ', ReplicaRole
        ),
        0,
        GETDATE()
    FROM dbo.fact_HADRSnapshot
    WHERE SnapshotTime >= DATEADD(MINUTE, -5, GETDATE())
      AND RoleChanged = 1
      AND NOT EXISTS (
            SELECT 1
            FROM dbo.fact_Alerts a
            WHERE a.AlertType = 'Failover Event'
              AND a.Processed = 0
              AND a.ServerName = fact_HADRSnapshot.ServerName
        );
END

GO

-- [dbo].[usp_Check_LongRunningQueries]
CREATE OR ALTER PROCEDURE dbo.usp_Check_LongRunningQueries
AS
BEGIN
    SET NOCOUNT ON;

    INSERT INTO dbo.fact_Alerts (ServerName, AlertType, AlertSeverity, AlertMessage)
    SELECT 
        @@SERVERNAME,
        'Long Running Query',
        'MEDIUM',
        CONCAT('Query running longer than 5 minutes: Session ', session_id)
    FROM sys.dm_exec_requests
    WHERE total_elapsed_time > 300000
      AND NOT EXISTS (
            SELECT 1 FROM dbo.fact_Alerts 
            WHERE AlertType='Long Running Query'
              AND Processed = 0
              AND ServerName = @@SERVERNAME
      );
END

GO

-- [dbo].[usp_Check_MemoryPressure]

CREATE   PROCEDURE dbo.usp_Check_MemoryPressure
AS
BEGIN
    SET NOCOUNT ON;

    INSERT INTO dbo.fact_Alerts
    (
        ServerName,
        AlertType,
        AlertSeverity,
        AlertMessage,
        AlertTime
    )
    SELECT
        ServerName,
        'Memory Pressure',
        'HIGH',
        CONCAT(
            'Memory pressure detected on ', ServerName, 
            '. SQL Memory Used: ', SqlMemoryUsedMB, ' MB. ',
            'Available OS Memory: ', AvailableMemoryMB, ' MB. ',
            'PLE: ', PageLifeExpectancy
        ),
        GETDATE()
    FROM dbo.fact_PerformanceSnapshot
    WHERE SnapshotTime >= DATEADD(MINUTE, -5, GETDATE())
      AND (
            AvailableMemoryMB * 1.0 / TotalMemoryMB < 0.15   -- Free < 15%
         OR SqlMemoryUsedMB  * 1.0 / TotalMemoryMB > 0.70   -- SQL > 70%
         OR PageLifeExpectancy < 600                        -- PLE < 600
      )
      AND NOT EXISTS
      (
          SELECT 1
          FROM dbo.fact_Alerts A
          WHERE A.AlertType = 'Memory Pressure'
            AND A.ServerName = fact_PerformanceSnapshot.ServerName
            AND A.Processed = 0
      );
END;

GO

-- [dbo].[usp_Check_TempDB]
CREATE OR ALTER PROCEDURE dbo.usp_Check_TempDB
AS
BEGIN
    SET NOCOUNT ON;

    INSERT INTO dbo.fact_Alerts (ServerName, AlertType, AlertSeverity, AlertMessage)
    SELECT 
        @@SERVERNAME,
        'TempDB Contention',
        'HIGH',
        'TempDB allocation or latch contention detected'
    FROM sys.dm_os_wait_stats
    WHERE wait_type LIKE 'PAGELATCH_%'
      AND waiting_tasks_count > 1000
      AND NOT EXISTS (
            SELECT 1 FROM dbo.fact_Alerts
            WHERE AlertType = 'TempDB Contention'
              AND Processed = 0
              AND ServerName = @@SERVERNAME
      );
END

GO

-- [dbo].[usp_CheckCPUAlerts]

CREATE   PROCEDURE dbo.usp_CheckCPUAlerts
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @CPUThreshold INT = 90;
    DECLARE @SustainedMinutes INT = 15;
    
    SELECT @CPUThreshold = CAST(ConfigValue AS INT) FROM dbo.Configuration WHERE ConfigKey = 'CPUAlertThreshold';
    SELECT @SustainedMinutes = CAST(ConfigValue AS INT) FROM dbo.Configuration WHERE ConfigKey = 'CPUAlertSustainedMinutes';
    
    -- Find servers with sustained high CPU
    SELECT 
        ServerName,
        AVG(CPUPercent) AS AvgCPU,
        MAX(CPUPercent) AS MaxCPU,
        COUNT(*) AS SampleCount,
        MIN(CaptureTime) AS FirstHighCPU,
        MAX(CaptureTime) AS LastHighCPU,
        DATEDIFF(MINUTE, MIN(CaptureTime), MAX(CaptureTime)) AS DurationMinutes
    FROM DBAOps.PerformanceTelemetry
    WHERE CaptureTime > DATEADD(MINUTE, -@SustainedMinutes, GETDATE())
      AND CPUPercent > @CPUThreshold
    GROUP BY ServerName
    HAVING COUNT(*) >= (@SustainedMinutes / 5)
    ORDER BY AVG(CPUPercent) DESC;
END

GO

-- [dbo].[usp_CheckDiskSpaceAlerts]

CREATE   PROCEDURE dbo.usp_CheckDiskSpaceAlerts
AS
BEGIN
    SET NOCOUNT ON;
    
    /*
    Disk Space Alert Logic (Updated per Kurt Dreyer feedback):
    
    Standard Disks (< 2TB):
      - Alert when usage > 85%
      
    Large Disks (>= 2TB):
      - Alert when usage > 90% AND free space < 500GB
      - This prevents unnecessary alerts when there's still plenty of free space
      
    Exclusions:
      - Skip disks listed in DiskSpaceExclusions table
    */
    
    DECLARE @StandardThreshold INT = 85;
    DECLARE @LargeDiskThreshold INT = 90;
    DECLARE @MinFreeGB INT = 500;
    DECLARE @LargeDiskSizeGB INT = 2000; -- 2TB
    
    -- Get thresholds from configuration if available
    SELECT @StandardThreshold = CAST(ConfigValue AS INT)
    FROM dbo.Configuration WHERE ConfigKey = 'DiskSpaceAlertThreshold';
    
    SELECT @LargeDiskThreshold = CAST(ConfigValue AS INT)
    FROM dbo.Configuration WHERE ConfigKey = 'DiskSpaceAlertThreshold_LargeDisks';
    
    SELECT @MinFreeGB = CAST(ConfigValue AS INT)
    FROM dbo.Configuration WHERE ConfigKey = 'DiskSpaceMinFreeGB_LargeDisks';
    
    -- Return disks that need alerts
    SELECT 
        d.ServerName,
        d.DriveLetter,
        d.TotalSizeGB,
        d.FreeSpaceGB,
        d.UsedPercent,
        CASE 
            WHEN d.TotalSizeGB >= @LargeDiskSizeGB THEN 'Large Disk (>2TB)'
            ELSE 'Standard Disk'
        END AS DiskCategory,
        CASE 
            WHEN d.TotalSizeGB >= @LargeDiskSizeGB 
                THEN CONCAT('Threshold: ', @LargeDiskThreshold, '% AND <', @MinFreeGB, 'GB free')
            ELSE CONCAT('Threshold: ', @StandardThreshold, '%')
        END AS AppliedThreshold,
        'ALERT' AS Status
    FROM dbo.DiskSpaceMetrics d  -- Adjust table name as needed
    LEFT JOIN dbo.DiskSpaceExclusions e 
        ON d.ServerName = e.ServerName 
        AND d.DriveLetter = e.DriveLetter
        AND e.IsActive = 1
    WHERE e.ExclusionID IS NULL  -- Not excluded
    AND (
        -- Standard disks: Alert at 85%
        (d.TotalSizeGB < @LargeDiskSizeGB AND d.UsedPercent > @StandardThreshold)
        OR
        -- Large disks: Alert at 90% AND less than 500GB free
        (d.TotalSizeGB >= @LargeDiskSizeGB AND d.UsedPercent > @LargeDiskThreshold AND d.FreeSpaceGB < @MinFreeGB)
    )
    ORDER BY d.UsedPercent DESC;
END

GO

-- [dbo].[usp_CheckMaintenanceHealth]

-- ============================================================================
-- usp_CheckMaintenanceHealth
-- ============================================================================
CREATE   PROCEDURE dbo.usp_CheckMaintenanceHealth
    @HoursBack INT = 24,
    @SendEmail BIT = 1
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @FailedJobs INT = 0;
    DECLARE @Message NVARCHAR(MAX) = '';
    
    -- Check for failed maintenance jobs in the last X hours
    SELECT @FailedJobs = COUNT(*)
    FROM msdb.dbo.sysjobhistory h
    JOIN msdb.dbo.sysjobs j ON h.job_id = j.job_id
    WHERE h.step_id = 0
      AND h.run_status = 0  -- Failed
      AND j.name LIKE '%Maintenance%' OR j.name LIKE '%Index%' OR j.name LIKE '%Integrity%'
      AND msdb.dbo.agent_datetime(h.run_date, h.run_time) >= DATEADD(HOUR, -@HoursBack, GETDATE());
    
    -- Log the check
    EXEC Logging.usp_WriteLog
        @LogLevel = 'INFO',
        @Category = 'Maintenance',
        @Operation = 'Health Check',
        @Message = 'Maintenance health check completed',
        @RowsAffected = @FailedJobs;
    
    -- Alert if failures found
    IF @FailedJobs > 0 AND @SendEmail = 1
    BEGIN
        SET @Message = CAST(@FailedJobs AS VARCHAR(10)) + ' maintenance job(s) failed in the last ' + CAST(@HoursBack AS VARCHAR(10)) + ' hours.';
        
        EXEC Alerting.usp_SendEmailAlert
            @Recipients = 'dbaopsmail@YOURORG.gov',
            @Subject = 'DBAOps Maintenance Health Alert',
            @Body = @Message,
            @Severity = 'Warning',
            @Category = 'Maintenance';
    END
    
    SELECT @FailedJobs AS FailedMaintenanceJobs;
END

GO

-- [dbo].[usp_CleanupDatabaseInventory]

-- ============================================================================
-- PROCEDURE 2: Cleanup/Archive Procedure
-- ============================================================================
CREATE   PROCEDURE dbo.usp_CleanupDatabaseInventory
    @ArchiveRetentionDays INT = 90,
    @Debug BIT = 0
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @DeletedRows INT = 0;
    
    -- Delete old archive data beyond retention
    DELETE FROM dbo.fact_DatabaseInventory_Archive
    WHERE ArchivedAt < DATEADD(DAY, -@ArchiveRetentionDays, SYSDATETIME());
    
    SET @DeletedRows = @@ROWCOUNT;
    
    IF @Debug = 1
        PRINT 'Deleted ' + CAST(@DeletedRows AS VARCHAR) + ' old archive rows';
    
    -- Return summary
    SELECT 
        @DeletedRows AS OldArchiveRowsDeleted,
        (SELECT COUNT(*) FROM dbo.fact_DatabaseInventory) AS CurrentInventoryRows,
        (SELECT COUNT(*) FROM dbo.fact_DatabaseInventory_Archive) AS ArchiveRows;
END;

GO

-- [dbo].[usp_CleanupMaintenanceLogs]

-- ============================================================================
-- usp_CleanupMaintenanceLogs
-- ============================================================================
CREATE   PROCEDURE dbo.usp_CleanupMaintenanceLogs
    @RetentionDays INT = 90
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @CutoffDate DATETIME = DATEADD(DAY, -@RetentionDays, GETDATE());
    DECLARE @Deleted INT = 0;
    
    -- Cleanup CommandLog (Ola Hallengren)
    IF OBJECT_ID('dbo.CommandLog', 'U') IS NOT NULL
    BEGIN
        DELETE FROM dbo.CommandLog WHERE StartTime < @CutoffDate;
        SET @Deleted = @Deleted + @@ROWCOUNT;
    END
    
    -- Cleanup Logging.OperationLog
    IF OBJECT_ID('Logging.OperationLog', 'U') IS NOT NULL
    BEGIN
        DELETE FROM Logging.OperationLog WHERE LogTimestamp < @CutoffDate;
        SET @Deleted = @Deleted + @@ROWCOUNT;
    END
    
    -- Log the cleanup
    EXEC Logging.usp_WriteLog
        @LogLevel = 'INFO',
        @Category = 'Maintenance',
        @Operation = 'Log Cleanup',
        @Message = 'Maintenance logs cleanup completed',
        @RowsAffected = @Deleted;
    
    SELECT @Deleted AS RowsDeleted;
END

GO

-- [dbo].[usp_Collect_AllServersHealth]

CREATE   PROCEDURE dbo.usp_Collect_AllServersHealth
AS
BEGIN
    SET NOCOUNT ON;
    
    SELECT 
        cms.ServerName,
        COALESCE(cms.Environment, cms.CMSEnvironment, 'Unknown') AS Environment,
        cms.SQLVersion,
        cms.ComplianceTier,
        cms.LastSeen
    FROM config.ServerInventory cms
    WHERE cms.IsActive = 1
    AND (cms.ServerType = 'SQL Server' OR cms.ServerType IS NULL)
    ORDER BY cms.Environment, cms.ServerName;
END

GO

-- [dbo].[usp_Collect_ConfigCompliance]

CREATE   PROCEDURE dbo.usp_Collect_ConfigCompliance
    @ServerName NVARCHAR(128) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    SELECT 
        cms.ServerName,
        COALESCE(cms.Environment, cms.CMSEnvironment, 'Unknown') AS Environment,
        cms.SQLVersion,
        cms.MaxDOP,
        cms.CostThreshold,
        cms.TotalMemoryGB,
        cms.CPUCount,
        cms.ComplianceTier
    FROM config.ServerInventory cms
    WHERE cms.IsActive = 1
    AND (cms.ServerType = 'SQL Server' OR cms.ServerType IS NULL)
    AND (@ServerName IS NULL OR cms.ServerName = @ServerName)
    ORDER BY cms.Environment, cms.ServerName;
END

GO

-- [dbo].[usp_Collect_DailyInventorySnapshot]

CREATE   PROCEDURE usp_Collect_DailyInventorySnapshot
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @ServerName sysname;
    DECLARE @SQL nvarchar(max);

    DECLARE ServerCursor CURSOR FAST_FORWARD FOR
    SELECT name
    FROM msdb.dbo.sysmanagement_shared_registered_servers_internal;

    OPEN ServerCursor;
    FETCH NEXT FROM ServerCursor INTO @ServerName;

    WHILE @@FETCH_STATUS = 0
    BEGIN
        SET @SQL = N'
        INSERT INTO fact_DatabaseInventory
        (
            ServerName,
            DatabaseName,
            RecoveryModel,
            CompatibilityLevel,
            SizeMB,
            CaptureTime
        )
        SELECT 
            ''' + @ServerName + N''' AS ServerName,
            name AS DatabaseName,
            recovery_model_desc AS RecoveryModel,
            compatibility_level AS CompatibilityLevel,
            CAST(SUM(size)/128.0 AS DECIMAL(10,2)) AS SizeMB,
            SYSDATETIME() AS CaptureTime
        FROM [' + @ServerName + N'].master.sys.databases d
        LEFT JOIN [' + @ServerName + N'].sys.master_files f
            ON d.database_id = f.database_id
        GROUP BY name, recovery_model_desc, compatibility_level;
        ';

        EXEC sys.sp_executesql @SQL;
        
        FETCH NEXT FROM ServerCursor INTO @ServerName;
    END

    CLOSE ServerCursor;
    DEALLOCATE ServerCursor;
END

GO

-- [dbo].[usp_Collect_DatabaseInventory]
-- 2ï¸âƒ£ Collect Database Inventory
CREATE   PROCEDURE usp_Collect_DatabaseInventory
AS
BEGIN
    SET NOCOUNT ON;

    INSERT INTO dim_DIM_Database (ServerName, DatabaseName, StateDesc, RecoveryModel, CreateDate, CaptureTime)
    SELECT  @@SERVERNAME,
            name,
            state_desc,
            recovery_model_desc,
            create_date,
            GETDATE()
    FROM master.sys.databases
    WHERE name NOT IN ('tempdb');  
END

GO

-- [dbo].[usp_Collect_DDL_AuditEvents]

CREATE PROCEDURE dbo.usp_Collect_DDL_AuditEvents
AS
BEGIN
    SET NOCOUNT ON;
    
    ;WITH XEDATA AS
    (
        SELECT
            event_data.value('(event/@name)[1]', 'varchar(50)') AS EventName,
            event_data.value('(event/data[@name="object_name"]/value)[1]','sysname') AS ObjectName,
            event_data.value('(event/data[@name="database_name"]/value)[1]','sysname') AS DatabaseName,
            event_data.value('(event/action[@name="server_principal_name"]/value)[1]','sysname') AS LoginName,
            event_data.value('(event/@timestamp)[1]','datetime2(3)') AS EventTime
        FROM
        (
            SELECT CAST(event_data AS XML) AS event_data
            FROM sys.fn_xe_file_target_read_file
            (
                'D:\SQLXE\DBAOps_DDL_Audit*.xel',
                NULL, NULL, NULL
            )
        ) X
    )
    INSERT INTO dbo.DDL_AuditEvents (EventName, DatabaseName, ObjectName, LoginName, EventTime)
    SELECT X.EventName, X.DatabaseName, X.ObjectName, X.LoginName, X.EventTime
    FROM XEDATA X
    WHERE X.DatabaseName IS NOT NULL
        AND X.DatabaseName <> ''
        AND X.DatabaseName <> 'tempdb'
        AND X.ObjectName IS NOT NULL
        AND X.ObjectName <> ''
        AND X.ObjectName NOT LIKE '#%'
        AND X.ObjectName NOT LIKE '_WA_Sys%'
        AND X.ObjectName <> 'T'
        AND LEN(X.ObjectName) > 1
        AND NOT EXISTS
        (
            SELECT 1 FROM dbo.DDL_AuditEvents D
            WHERE D.EventName = X.EventName
              AND D.DatabaseName = X.DatabaseName
              AND D.ObjectName = X.ObjectName
              AND D.LoginName = X.LoginName
              AND D.EventTime = X.EventTime
        );
    
    PRINT 'Inserted ' + CAST(@@ROWCOUNT AS VARCHAR) + ' DDL audit events';
END;

GO

-- [dbo].[usp_Collect_DDL_AuditEvents_WithReport]
-- ============================================================================
-- FIX: Update DDL Collection procedure to handle empty DatabaseName
-- ============================================================================
CREATE   PROCEDURE dbo.usp_Collect_DDL_AuditEvents_WithReport
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @InsertedCount INT = 0;
    DECLARE @Subject NVARCHAR(255);
    DECLARE @TableHTML NVARCHAR(MAX);
    DECLARE @Recipients NVARCHAR(500) = N'DBAOps@YOURORG.gov; SQLServerAdminNotification@YOURORG.gov';
    
    -- Create temp table to capture inserted events
    CREATE TABLE #NewEvents (
        EventName VARCHAR(50),
        DatabaseName SYSNAME NULL,
        ObjectName SYSNAME,
        ObjectType VARCHAR(50),
        LoginName SYSNAME,
        EventTime DATETIME2(3),
        SQLText NVARCHAR(500)
    );
    
    -- Collect DDL events - FIXED: Handle empty DatabaseName, filter by ObjectType
    ;WITH XEDATA AS
    (
        SELECT
            event_data.value('(event/@name)[1]', 'varchar(50)') AS EventName,
            event_data.value('(event/data[@name="object_name"]/value)[1]','sysname') AS ObjectName,
            NULLIF(event_data.value('(event/data[@name="database_name"]/value)[1]','sysname'), '') AS DatabaseName,
            event_data.value('(event/data[@name="object_type"]/text)[1]','varchar(50)') AS ObjectType,
            event_data.value('(event/action[@name="server_principal_name"]/value)[1]','sysname') AS LoginName,
            event_data.value('(event/@timestamp)[1]','datetime2(3)') AS EventTime,
            LEFT(event_data.value('(event/action[@name="sql_text"]/value)[1]','nvarchar(max)'), 500) AS SQLText
        FROM
        (
            SELECT CAST(event_data AS XML) AS event_data
            FROM sys.fn_xe_file_target_read_file
            (
                'D:\SQLXE\DBAOps_DDL_Audit*.xel',
                NULL, NULL, NULL
            )
        ) X
    )
    INSERT INTO dbo.DDL_AuditEvents (EventName, DatabaseName, ObjectName, LoginName, EventTime)
    OUTPUT INSERTED.EventName, INSERTED.DatabaseName, INSERTED.ObjectName, NULL, INSERTED.LoginName, INSERTED.EventTime, NULL
    INTO #NewEvents (EventName, DatabaseName, ObjectName, ObjectType, LoginName, EventTime, SQLText)
    SELECT X.EventName, ISNULL(X.DatabaseName, 'DBAOpsRepository'), X.ObjectName, X.LoginName, X.EventTime
    FROM XEDATA X
    WHERE X.ObjectName IS NOT NULL
        AND X.ObjectName <> ''
        AND X.ObjectName NOT LIKE '#%'           -- Exclude temp objects
        AND X.ObjectName NOT LIKE '_WA_Sys%'     -- Exclude auto-stats
        AND X.ObjectName <> 'T'                  -- Exclude statistics
        AND LEN(X.ObjectName) > 2                -- Exclude very short names
        AND X.ObjectType IN ('PROC', 'USRTAB', 'INDEX', 'SCHEMA', 'VIEW', 'FUNCTION', 'TRIGGER')  -- Only real objects
        AND X.LoginName NOT LIKE 'NT SERVICE%'  -- Exclude system accounts
        AND NOT EXISTS
        (
            SELECT 1 FROM dbo.DDL_AuditEvents D
            WHERE D.EventName = X.EventName
              AND ISNULL(D.DatabaseName, '') = ISNULL(ISNULL(X.DatabaseName, 'DBAOpsRepository'), '')
              AND D.ObjectName = X.ObjectName
              AND D.LoginName = X.LoginName
              AND D.EventTime = X.EventTime
        );
    
    SET @InsertedCount = @@ROWCOUNT;
    PRINT 'Inserted ' + CAST(@InsertedCount AS VARCHAR) + ' DDL audit events';
    
    -- Send report if events were detected
    IF @InsertedCount > 0
    BEGIN
        SET @TableHTML = N'<html>
<head>
<style>
    body { font-family: Arial, sans-serif; }
    h2 { color: #2E86AB; }
    table { border-collapse: collapse; width: 100%; margin-top: 10px; }
    th { background-color: #2E86AB; color: white; padding: 10px; text-align: left; }
    td { padding: 8px; border: 1px solid #ddd; }
    tr:nth-child(even) { background-color: #f9f9f9; }
    .created { color: green; font-weight: bold; }
    .deleted { color: red; font-weight: bold; }
    .altered { color: orange; font-weight: bold; }
    .summary { background-color: #e8f4f8; padding: 15px; border-radius: 5px; margin-bottom: 15px; }
</style>
</head>
<body>
<h2>ðŸ” DDL Audit Report - Schema Changes Detected</h2>
<div class="summary">
    <strong>Server:</strong> ' + @@SERVERNAME + N'<br/>
    <strong>Report Time:</strong> ' + CONVERT(NVARCHAR, GETDATE(), 120) + N'<br/>
    <strong>Events Detected:</strong> ' + CAST(@InsertedCount AS NVARCHAR) + N'
</div>

<h3>Event Summary</h3>
<table>
<tr><th>Event Type</th><th>Count</th></tr>';

        SELECT @TableHTML = @TableHTML + 
            N'<tr><td class="' +
            CASE 
                WHEN EventName LIKE '%created%' THEN 'created'
                WHEN EventName LIKE '%deleted%' OR EventName LIKE '%dropped%' THEN 'deleted'
                WHEN EventName LIKE '%altered%' THEN 'altered'
                ELSE ''
            END + '">' + EventName + N'</td><td>' + CAST(COUNT(*) AS NVARCHAR) + N'</td></tr>'
        FROM #NewEvents
        GROUP BY EventName
        ORDER BY COUNT(*) DESC;

        SET @TableHTML = @TableHTML + N'</table>

<h3>Event Details</h3>
<table>
<tr><th>Event Time</th><th>Event</th><th>Database</th><th>Object</th><th>Login</th></tr>';

        SELECT TOP 50 @TableHTML = @TableHTML + 
            N'<tr>
                <td>' + CONVERT(NVARCHAR, EventTime, 120) + N'</td>
                <td class="' + 
                    CASE 
                        WHEN EventName LIKE '%created%' THEN 'created'
                        WHEN EventName LIKE '%deleted%' OR EventName LIKE '%dropped%' THEN 'deleted'
                        WHEN EventName LIKE '%altered%' THEN 'altered'
                        ELSE ''
                    END + '">' + EventName + N'</td>
                <td>' + ISNULL(DatabaseName, 'DBAOpsRepository') + N'</td>
                <td>' + ISNULL(ObjectName, '') + N'</td>
                <td>' + ISNULL(LoginName, '') + N'</td>
            </tr>'
        FROM #NewEvents
        ORDER BY EventTime DESC;

        SET @TableHTML = @TableHTML + N'</table>';

        IF @InsertedCount > 50
            SET @TableHTML = @TableHTML + N'<p><em>Note: Showing first 50 of ' + CAST(@InsertedCount AS NVARCHAR) + N' events.</em></p>';

        SET @TableHTML = @TableHTML + N'
<br/>
<p style="color:gray;">DBAOps Monitoring Framework | Author: John Bosco RHOYLLE INNOVATION EMPIRE™</p>
</body></html>';

        SET @Subject = N'ðŸ” DDL Audit: ' + CAST(@InsertedCount AS NVARCHAR) + N' Schema Changes - ' + @@SERVERNAME;

        EXEC msdb.dbo.sp_send_dbmail
            @profile_name = 'DBAOpsProfile',
            @recipients = @Recipients,
            @subject = @Subject,
            @body = @TableHTML,
            @body_format = 'HTML';
        
        PRINT 'DDL Audit report sent';
    END
    ELSE
        PRINT 'No new DDL events detected';
    
    DROP TABLE #NewEvents;
END;

GO

-- [dbo].[usp_Collect_FullServerInventory]

-- 1ï¸âƒ£ Collect Server Inventory
CREATE   PROCEDURE usp_Collect_FullServerInventory
AS
BEGIN
    SET NOCOUNT ON;

    INSERT INTO dim_DIM_Server (ServerName, DomainName, EnvironmentName, CaptureTime)
    SELECT  @@SERVERNAME,
            DEFAULT_DOMAIN(),
            CASE WHEN SERVERPROPERTY('IsClustered') = 1 THEN 'PROD' ELSE 'NONPROD' END,
            GETDATE();
END

GO

-- [dbo].[usp_Collect_JobIntegrityAudit]

CREATE   PROCEDURE dbo.usp_Collect_JobIntegrityAudit
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @InstanceName NVARCHAR(128) = @@SERVERNAME;

    ;WITH JobCore AS (
        SELECT 
            j.job_id,
            j.name AS JobName,
            j.enabled AS IsJobEnabled,
            s.name AS ScheduleName,
            s.enabled AS IsScheduleEnabled,
            CASE WHEN s.schedule_id IS NULL THEN 0 ELSE 1 END AS IsScheduled
        FROM msdb.dbo.sysjobs j
        LEFT JOIN msdb.dbo.sysjobschedules js ON j.job_id = js.job_id
        LEFT JOIN msdb.dbo.sysschedules s ON js.schedule_id = s.schedule_id
    ),
    JobRuns AS (
        SELECT 
            job_id,
            MAX(run_date) AS LastRunDate,
            MAX(run_time) AS LastRunTime,
            MAX(run_duration) AS RunDuration,
            MAX(run_status) AS RunStatus
        FROM msdb.dbo.sysjobhistory
        GROUP BY job_id
    ),
    JobNextRun AS (
        SELECT 
            job_id,
            next_run_date,
            next_run_time
        FROM msdb.dbo.sysjobschedules
    )

    INSERT INTO dbo.JobIntegrityAudit (
        SQLInstance, JobName, ScheduleName, IsScheduled, 
        IsScheduleEnabled, IsJobEnabled, HasDuplicateSched,
        LastRunDate, LastRunStatus, RunDuration, NextRunDate,
        IssueDetected
    )
    SELECT 
        @InstanceName,
        c.JobName,
        c.ScheduleName,
        c.IsScheduled,
        c.IsScheduleEnabled,
        c.IsJobEnabled,
        0 AS HasDuplicateSched,

        CASE 
            WHEN r.LastRunDate IS NULL THEN NULL 
            ELSE CONVERT(DATETIME, 
                    STUFF(STUFF(CONVERT(VARCHAR(8), r.LastRunDate), 5, 0, '-'), 8, 0, '-') 
                + ' ' +
                    STUFF(STUFF(RIGHT('000000' + CONVERT(VARCHAR(6), r.LastRunTime), 6), 3, 0, ':'), 6, 0, ':')
            )
        END AS LastRunDate,

        CASE r.RunStatus 
            WHEN 0 THEN 'Failed'
            WHEN 1 THEN 'Succeeded'
            WHEN 2 THEN 'Retry'
            WHEN 3 THEN 'Cancelled'
            ELSE 'Unknown'
        END AS LastRunStatus,

        r.RunDuration,

        CASE 
            WHEN nr.next_run_date = 0 THEN NULL
            ELSE CONVERT(DATETIME,
                STUFF(STUFF(CONVERT(VARCHAR(8), nr.next_run_date),5,0,'-'),8,0,'-')
                + ' '
                + STUFF(STUFF(RIGHT('000000' + CONVERT(VARCHAR(6), nr.next_run_time),6),3,0,':'),6,0,':')
            )
        END AS NextRunDate,

        CASE
            WHEN c.IsJobEnabled = 0 THEN 'Job Disabled'
            WHEN c.IsScheduled = 0 THEN 'Job Has No Schedule'
            WHEN c.IsScheduleEnabled = 0 THEN 'Schedule Disabled'
            WHEN r.RunStatus = 0 THEN 'Last Run Failed'
            WHEN r.LastRunDate IS NULL THEN 'Job Never Executed'
            ELSE 'Healthy'
        END AS IssueDetected
    FROM JobCore c
    LEFT JOIN JobRuns r ON c.job_id = r.job_id
    LEFT JOIN JobNextRun nr ON c.job_id = nr.job_id;

END

GO

-- [dbo].[usp_Collect_JobScheduleCompliance]

CREATE   PROCEDURE dbo.usp_Collect_JobScheduleCompliance
AS
BEGIN
    SET NOCOUNT ON;

    INSERT INTO fact.JobCompliance (
        JobName,
        ExpectedSchedule,
        ActualSchedule,
        ScheduleID,
        IsScheduleEnabled,
        ComplianceStatus,
        CaptureDate
    )
    SELECT 
        JobName,
        ExpectedSchedule,
        actualSchedule,
        schedule_id,
        enabled,
        ComplianceStatus,
        CaptureDate
    FROM dbo.vJobScheduleCompliance;
END

GO

-- [dbo].[usp_Collect_PerformanceBaseline]

CREATE   PROCEDURE dbo.usp_Collect_PerformanceBaseline
    @ServerName NVARCHAR(128) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    SELECT 
        cms.ServerName,
        COALESCE(cms.Environment, cms.CMSEnvironment, 'Unknown') AS Environment,
        cms.SQLVersion,
        cms.CPUCount,
        cms.TotalMemoryGB,
        cms.MaxDOP,
        cms.CostThreshold
    FROM config.ServerInventory cms
    WHERE cms.IsActive = 1
    AND (cms.ServerType = 'SQL Server' OR cms.ServerType IS NULL)
    AND (@ServerName IS NULL OR cms.ServerName = @ServerName)
    ORDER BY cms.Environment, cms.ServerName;
END

GO

-- [dbo].[usp_Collect_PerformanceCompliance_Centralized]

CREATE   PROCEDURE dbo.usp_Collect_PerformanceCompliance_Centralized
AS
BEGIN
    SET NOCOUNT ON;
    
    SELECT 
        cms.ServerName,
        COALESCE(cms.Environment, cms.CMSEnvironment, 'Unknown') AS Environment,
        cms.SQLVersion,
        cms.CPUCount,
        cms.TotalMemoryGB,
        cms.MaxDOP,
        cms.CostThreshold,
        cms.ComplianceTier
    FROM config.ServerInventory cms
    WHERE cms.IsActive = 1
    AND (cms.ServerType = 'SQL Server' OR cms.ServerType IS NULL)
    ORDER BY cms.Environment, cms.ServerName;
END

GO

-- [dbo].[usp_Collect_ServerHealth_Remote]
CREATE OR ALTER PROCEDURE dbo.usp_Collect_ServerHealth_Remote
    @TargetServer SYSNAME
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @cmd NVARCHAR(MAX);

    /*
       SQLCMD retrieves metrics from remote SQL Server without linked servers
       Output is converted to a table and inserted into FACT_ServerHealth
    */

    SET @cmd = '
        sqlcmd -S ' + QUOTENAME(@TargetServer, '"') + ' -E -Q "
        SET NOCOUNT ON;
        SELECT
            @@SERVERNAME AS ServerName,
            SYSDATETIME() AS CaptureTime,
            (SELECT TOP 1 cntr_value FROM sys.dm_os_performance_counters WHERE counter_name LIKE ''%Processor Time%'' AND instance_name = ''_Total'') AS CPU_Utilization,
            (SELECT TOP 1 total_physical_memory_kb FROM sys.dm_os_sys_memory) / 1024 AS Memory_TotalMB,
            (SELECT TOP 1 available_physical_memory_kb FROM sys.dm_os_sys_memory) / 1024 AS Memory_AvailableMB,
            (SELECT SUM(user_object_reserved_page_count + internal_object_reserved_page_count)*8 FROM tempdb.sys.dm_db_file_space_usage) AS TempDB_UsageMB,
            (SELECT COUNT(*) FROM sys.dm_exec_sessions WHERE is_user_process = 1) AS ActiveSessions,
            (SELECT COUNT(*) FROM sys.dm_exec_requests WHERE blocking_session_id <> 0) AS BlockingSessions,
            (SELECT COUNT(*) FROM sys.dm_exec_requests WHERE total_elapsed_time > 60000) AS LongRunningRequests,
            (SELECT @@VERSION) AS InstanceVersion;
        ";
    ';

    CREATE TABLE #RemoteHealthRaw (OutputLine NVARCHAR(MAX));

    INSERT INTO #RemoteHealthRaw
    EXEC xp_cmdshell @cmd;

    -- Clean the result
    INSERT INTO dbo.FACT_ServerHealth
    SELECT TOP 1
        @TargetServer,
        GETDATE(),
        NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL
    FROM #RemoteHealthRaw; -- placeholder mapping

END

GO

-- [dbo].[usp_Collect_WindowsUpdateCompliance]

CREATE   PROCEDURE dbo.usp_Collect_WindowsUpdateCompliance
    @ServerName NVARCHAR(128) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    SELECT 
        cms.ServerName,
        COALESCE(cms.Environment, cms.CMSEnvironment, 'Unknown') AS Environment,
        cms.OSVersion,
        cms.WindowsVersion,
        cms.WindowsBuild,
        cms.LastPatchDate,
        cms.ComplianceTier
    FROM config.ServerInventory cms
    WHERE cms.IsActive = 1
    AND (@ServerName IS NULL OR cms.ServerName = @ServerName)
    ORDER BY cms.Environment, cms.ServerName;
END

GO

-- [dbo].[usp_CollectBlockingEvents]
-- Fix: Complete Blocking Monitor Procedure (CMS as source of truth)
-- Run in SSMS on YOURSERVER01 against DBAOpsRepository

CREATE PROCEDURE dbo.usp_CollectBlockingEvents
    @ServerName NVARCHAR(128) = NULL,
    @Debug BIT = 0
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @CurrentServer NVARCHAR(128);
    DECLARE @LinkedServer NVARCHAR(128);
    DECLARE @SQL NVARCHAR(MAX);
    DECLARE @ErrorMessage NVARCHAR(4000);
    DECLARE @ServerCount INT = 0;
    DECLARE @BlockingFound INT = 0;
    
    IF @Debug = 1
        PRINT '=== DBAOps Blocking Monitor - Started: ' + CONVERT(VARCHAR(30), GETDATE(), 120) + ' ===';
    
    -- =====================================================
    -- Get servers from CMS - SOURCE OF TRUTH
    -- =====================================================
    CREATE TABLE #ServersToMonitor (
        ServerName NVARCHAR(128)
    );
    
    INSERT INTO #ServersToMonitor (ServerName)
    SELECT server_name
    FROM msdb.dbo.sysmanagement_shared_registered_servers_internal
    WHERE server_name NOT LIKE '%\%'
      AND server_name <> @@SERVERNAME
      AND (@ServerName IS NULL OR server_name = @ServerName);
    
    SELECT @ServerCount = COUNT(*) FROM #ServersToMonitor;
    
    IF @Debug = 1
    BEGIN
        PRINT 'Servers to monitor from CMS: ' + CAST(@ServerCount AS VARCHAR(10));
        SELECT * FROM #ServersToMonitor ORDER BY ServerName;
    END
    
    -- =====================================================
    -- Cursor through each server
    -- =====================================================
    DECLARE server_cursor CURSOR LOCAL FAST_FORWARD FOR
        SELECT ServerName FROM #ServersToMonitor ORDER BY ServerName;
    
    OPEN server_cursor;
    FETCH NEXT FROM server_cursor INTO @CurrentServer;
    
    WHILE @@FETCH_STATUS = 0
    BEGIN
        SET @LinkedServer = 'DBAOps_Temp_' + REPLACE(@CurrentServer, '.', '_');
        
        BEGIN TRY
            IF @Debug = 1
                PRINT 'Checking: ' + @CurrentServer;
            
            -- Drop temp linked server if exists
            IF EXISTS (SELECT 1 FROM sys.servers WHERE name = @LinkedServer)
                EXEC sp_dropserver @LinkedServer, 'droplogins';
            
            -- Create temporary linked server with TrustServerCertificate
            EXEC sp_addlinkedserver 
                @server = @LinkedServer,
                @srvproduct = '',
                @provider = 'MSOLEDBSQL19',
                @datasrc = @CurrentServer,
                @provstr = 'TrustServerCertificate=yes';
            
            EXEC sp_addlinkedsrvlogin 
                @rmtsrvname = @LinkedServer,
                @useself = 'True',
                @locallogin = NULL;
            
            -- Collect blocking data
            SET @SQL = N'
            INSERT INTO audit.BlockingEvents (
                ServerName, EventTime, 
                BlockedSPID, BlockedLoginName, BlockedHostName, BlockedDatabase, 
                BlockedWaitTime, BlockedCommand, BlockedSQL,
                BlockingSPID, BlockingLoginName, BlockingHostName, BlockingDatabase,
                BlockingCommand, BlockingSQL, CollectedAt
            )
            SELECT 
                @Server,
                GETDATE(),
                blocked.session_id,
                blocked_sess.login_name,
                blocked_sess.host_name,
                DB_NAME(blocked.database_id),
                blocked.wait_time / 1000,
                blocked.command,
                CAST(blocked_text.text AS NVARCHAR(4000)),
                blocking.session_id,
                blocking_sess.login_name,
                blocking_sess.host_name,
                DB_NAME(blocking.database_id),
                blocking.command,
                CAST(blocking_text.text AS NVARCHAR(4000)),
                GETDATE()
            FROM [' + @LinkedServer + '].master.sys.dm_exec_requests blocked
            INNER JOIN [' + @LinkedServer + '].master.sys.dm_exec_sessions blocked_sess 
                ON blocked.session_id = blocked_sess.session_id
            LEFT JOIN [' + @LinkedServer + '].master.sys.dm_exec_requests blocking 
                ON blocked.blocking_session_id = blocking.session_id
            LEFT JOIN [' + @LinkedServer + '].master.sys.dm_exec_sessions blocking_sess 
                ON blocked.blocking_session_id = blocking_sess.session_id
            OUTER APPLY [' + @LinkedServer + '].master.sys.dm_exec_sql_text(blocked.sql_handle) blocked_text
            OUTER APPLY [' + @LinkedServer + '].master.sys.dm_exec_sql_text(blocking.sql_handle) blocking_text
            WHERE blocked.blocking_session_id <> 0
              AND blocked.wait_time > 5000;  -- Only blocking > 5 seconds
            ';
            
            EXEC sp_executesql @SQL, N'@Server NVARCHAR(128)', @Server = @CurrentServer;
            
            SET @BlockingFound = @BlockingFound + @@ROWCOUNT;
            
            -- Cleanup linked server
            EXEC sp_dropserver @LinkedServer, 'droplogins';
            
        END TRY
        BEGIN CATCH
            SET @ErrorMessage = ERROR_MESSAGE();
            IF @Debug = 1
                PRINT '  ERROR on ' + @CurrentServer + ': ' + @ErrorMessage;
            
            -- Cleanup on error
            BEGIN TRY
                IF EXISTS (SELECT 1 FROM sys.servers WHERE name = @LinkedServer)
                    EXEC sp_dropserver @LinkedServer, 'droplogins';
            END TRY
            BEGIN CATCH
            END CATCH
        END CATCH
        
        FETCH NEXT FROM server_cursor INTO @CurrentServer;
    END
    
    CLOSE server_cursor;
    DEALLOCATE server_cursor;
    
    DROP TABLE #ServersToMonitor;
    
    IF @Debug = 1
    BEGIN
        PRINT '';
        PRINT '=== Blocking Monitor Complete ===';
        PRINT 'Servers checked: ' + CAST(@ServerCount AS VARCHAR(10));
        PRINT 'Blocking events found: ' + CAST(@BlockingFound AS VARCHAR(10));
    END
END

GO

-- [dbo].[usp_CollectDatabaseInventory]

CREATE   PROCEDURE dbo.usp_CollectDatabaseInventory
    @ServerName NVARCHAR(128) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    -- Get servers to collect from CMS
    DECLARE @Servers TABLE (ServerName NVARCHAR(128), Environment NVARCHAR(50));
    
    INSERT INTO @Servers (ServerName, Environment)
    SELECT 
        ServerName,
        COALESCE(Environment, CMSEnvironment, 'Unknown')
    FROM config.ServerInventory
    WHERE IsActive = 1
    AND (ServerType = 'SQL Server' OR ServerType IS NULL)
    AND (@ServerName IS NULL OR ServerName = @ServerName);
    
    -- Return servers for collection
    SELECT ServerName, Environment FROM @Servers ORDER BY Environment, ServerName;
    
    PRINT 'Database inventory collection targets: ' + CAST(@@ROWCOUNT AS VARCHAR) + ' servers';
END

GO

-- [dbo].[usp_CollectDDLAuditEvents_Fixed]

-- The issue is likely SET ANSI_NULLS and QUOTED_IDENTIFIER not being ON
-- Let's create a wrapper procedure that sets these properly

CREATE   PROCEDURE dbo.usp_CollectDDLAuditEvents_Fixed
AS
BEGIN
    SET NOCOUNT ON;
    SET ANSI_NULLS ON;
    SET QUOTED_IDENTIFIER ON;
    SET ANSI_PADDING ON;
    SET ANSI_WARNINGS ON;
    SET CONCAT_NULL_YIELDS_NULL ON;
    
    -- Check if target table exists
    IF OBJECT_ID('security.DDLAuditLog', 'U') IS NULL
    BEGIN
        PRINT 'Target table security.DDLAuditLog does not exist';
        RETURN;
    END
    
    -- Check if Extended Events session exists
    IF NOT EXISTS (SELECT 1 FROM sys.dm_xe_sessions WHERE name = 'DBAOps_DDL_Audit')
    BEGIN
        PRINT 'Extended Events session DBAOps_DDL_Audit not found';
        RETURN;
    END
    
    BEGIN TRY
        -- Collect from XE session ring buffer
        ;WITH XEData AS (
            SELECT 
                CAST(target_data AS XML) AS TargetData
            FROM sys.dm_xe_sessions s
            INNER JOIN sys.dm_xe_session_targets t ON s.address = t.event_session_address
            WHERE s.name = 'DBAOps_DDL_Audit'
              AND t.target_name = 'ring_buffer'
        ),
        Events AS (
            SELECT 
                evt.value('(@name)[1]', 'NVARCHAR(100)') AS EventType,
                evt.value('(@timestamp)[1]', 'DATETIME2') AS EventTime,
                evt.value('(data[@name="server_principal_name"]/value)[1]', 'NVARCHAR(256)') AS LoginName,
                evt.value('(data[@name="database_name"]/value)[1]', 'NVARCHAR(256)') AS DatabaseName,
                evt.value('(data[@name="object_name"]/value)[1]', 'NVARCHAR(256)') AS ObjectName,
                evt.value('(data[@name="object_type"]/value)[1]', 'NVARCHAR(100)') AS ObjectType,
                evt.value('(data[@name="schema_name"]/value)[1]', 'NVARCHAR(256)') AS SchemaName,
                evt.value('(data[@name="statement"]/value)[1]', 'NVARCHAR(MAX)') AS TSQLCommand
            FROM XEData
            CROSS APPLY TargetData.nodes('//RingBufferTarget/event') AS xevents(evt)
        )
        INSERT INTO security.DDLAuditLog (ServerName, DatabaseName, EventType, ObjectName, ObjectType, SchemaName, LoginName, UserName, EventTime, TSQLCommand, CollectedDate)
        SELECT 
            @@SERVERNAME,
            ISNULL(DatabaseName, 'N/A'),
            ISNULL(EventType, 'UNKNOWN'),
            ISNULL(ObjectName, 'N/A'),
            ISNULL(ObjectType, 'N/A'),
            ISNULL(SchemaName, 'N/A'),
            ISNULL(LoginName, 'SYSTEM'),
            ISNULL(LoginName, 'SYSTEM'),
            ISNULL(EventTime, SYSDATETIME()),
            ISNULL(LEFT(TSQLCommand, 4000), 'N/A'),
            SYSDATETIME()
        FROM Events e
        WHERE NOT EXISTS (
            SELECT 1 FROM security.DDLAuditLog l
            WHERE l.EventTime = e.EventTime 
              AND l.ObjectName = e.ObjectName
              AND l.EventType = e.EventType
        );
        
        PRINT 'DDL Audit collection complete. Rows inserted: ' + CAST(@@ROWCOUNT AS VARCHAR);
        
    END TRY
    BEGIN CATCH
        PRINT 'Error in DDL Audit collection: ' + ERROR_MESSAGE();
        -- Don't re-throw, just log
    END CATCH
END;

GO

-- [dbo].[usp_CollectDiskSpace]

CREATE   PROCEDURE dbo.usp_CollectDiskSpace
    @WarningThresholdPercent DECIMAL(5,2) = 20.00,
    @CriticalThresholdPercent DECIMAL(5,2) = 10.00,
    @SendAlertEmail BIT = 1,
    @Debug BIT = 0
AS
BEGIN
    /***************************************************************************
    * Collects disk space from all CMS-registered servers
    * Uses the CMS server groups to execute queries across all servers
    * 
    * NOTE: This procedure should be called from a CMS multiserver query
    *       OR it will collect from the local server only
    ***************************************************************************/
    SET NOCOUNT ON;
    
    DECLARE @StartTime DATETIME2 = SYSDATETIME();
    DECLARE @AlertCount INT = 0;
    DECLARE @EmailBody NVARCHAR(MAX) = '';
    
    IF @Debug = 1
        PRINT 'Starting disk space collection at ' + CONVERT(VARCHAR, @StartTime, 121);
    
    -- Collect disk space from current server (works for both local and CMS multiserver)
    -- When run as CMS multiserver query, @@SERVERNAME returns each target server
    
    ;WITH DiskData AS (
        SELECT 
            @@SERVERNAME AS ServerName,
            LEFT(vs.volume_mount_point, 1) AS DriveLetter,
            vs.logical_volume_name AS VolumeName,
            CAST(vs.total_bytes / 1073741824.0 AS DECIMAL(18,2)) AS TotalSizeGB,
            CAST(vs.available_bytes / 1073741824.0 AS DECIMAL(18,2)) AS FreeSpaceGB,
            CAST(vs.available_bytes * 100.0 / NULLIF(vs.total_bytes, 0) AS DECIMAL(5,2)) AS FreeSpacePercent
        FROM sys.master_files mf
        CROSS APPLY sys.dm_os_volume_stats(mf.database_id, mf.file_id) vs
        GROUP BY vs.volume_mount_point, vs.logical_volume_name, vs.total_bytes, vs.available_bytes
    )
    MERGE dbo.DiskSpaceInventory AS Target
    USING DiskData AS Source
    ON Target.ServerName = Source.ServerName 
       AND Target.DriveLetter = Source.DriveLetter
    
    WHEN MATCHED THEN
        UPDATE SET 
            VolumeName = Source.VolumeName,
            TotalSizeGB = Source.TotalSizeGB,
            FreeSpaceGB = Source.FreeSpaceGB,
            FreeSpacePercent = Source.FreeSpacePercent,
            CollectedAt = SYSDATETIME()
    
    WHEN NOT MATCHED BY TARGET THEN
        INSERT (ServerName, DriveLetter, VolumeName, TotalSizeGB, FreeSpaceGB, FreeSpacePercent)
        VALUES (Source.ServerName, Source.DriveLetter, Source.VolumeName, 
                Source.TotalSizeGB, Source.FreeSpaceGB, Source.FreeSpacePercent);
    
    IF @Debug = 1
        PRINT 'Collected disk space from: ' + @@SERVERNAME;
    
    -- Check for alerts on this server
    INSERT INTO dbo.DiskSpaceAlerts (ServerName, DriveLetter, FreeSpaceGB, FreeSpacePercent, AlertSeverity)
    SELECT 
        ServerName,
        DriveLetter,
        FreeSpaceGB,
        FreeSpacePercent,
        CASE 
            WHEN FreeSpacePercent < @CriticalThresholdPercent THEN 'CRITICAL'
            ELSE 'WARNING'
        END
    FROM dbo.DiskSpaceInventory
    WHERE ServerName = @@SERVERNAME
      AND FreeSpacePercent < @WarningThresholdPercent
      AND NOT EXISTS (
          SELECT 1 FROM dbo.DiskSpaceAlerts a
          WHERE a.ServerName = dbo.DiskSpaceInventory.ServerName
            AND a.DriveLetter = dbo.DiskSpaceInventory.DriveLetter
            AND a.Acknowledged = 0
            AND a.AlertTime > DATEADD(HOUR, -4, SYSDATETIME())
      );
    
    SET @AlertCount = @@ROWCOUNT;
    
    -- Send alert if critical/warning found
    IF @AlertCount > 0 AND @SendAlertEmail = 1
    BEGIN
        SET @EmailBody = 'LOW DISK SPACE DETECTED:' + CHAR(13) + CHAR(10) + CHAR(13) + CHAR(10);
        
        SELECT @EmailBody = @EmailBody + 
            '[' + AlertSeverity + '] ' + ServerName + ' ' + DriveLetter + ': - ' + 
            CAST(FreeSpaceGB AS VARCHAR) + ' GB free (' + 
            CAST(FreeSpacePercent AS VARCHAR) + '%)' + CHAR(13) + CHAR(10)
        FROM dbo.DiskSpaceAlerts
        WHERE ServerName = @@SERVERNAME
          AND AlertTime > DATEADD(MINUTE, -5, SYSDATETIME())
        ORDER BY FreeSpacePercent ASC;
        
        EXEC msdb.dbo.sp_send_dbmail
            @profile_name = 'DBAOpsProfile',
            @recipients = 'DBAOps@YOURORG.gov; SQLServerAdminNotification@YOURORG.gov',
            @subject = '*** SQL ALERT: Low Disk Space Detected ***',
            @body = @EmailBody,
            @body_format = 'TEXT';
    END
    
    -- Return results for this server
    SELECT 
        ServerName,
        DriveLetter,
        VolumeName,
        TotalSizeGB,
        FreeSpaceGB,
        FreeSpacePercent,
        CASE 
            WHEN FreeSpacePercent < @CriticalThresholdPercent THEN 'CRITICAL'
            WHEN FreeSpacePercent < @WarningThresholdPercent THEN 'WARNING'
            ELSE 'OK'
        END AS Status
    FROM dbo.DiskSpaceInventory
    WHERE ServerName = @@SERVERNAME
    ORDER BY DriveLetter;
END;

GO

-- [dbo].[usp_CollectDiskSpace_Agent]

CREATE   PROCEDURE dbo.usp_CollectDiskSpace_Agent
    @WarningThresholdPercent DECIMAL(5,2) = 20.00,
    @CriticalThresholdPercent DECIMAL(5,2) = 10.00,
    @SendAlertEmail BIT = 1,
    @Debug BIT = 0
AS
BEGIN
    /***************************************************************************
    * Wrapper procedure for SQL Agent job
    * Collects local server disk space and reports summary
    * 
    * For full CMS collection, run usp_CollectDiskSpace as CMS multiserver query
    ***************************************************************************/
    SET NOCOUNT ON;
    
    -- Collect local server
    EXEC dbo.usp_CollectDiskSpace 
        @WarningThresholdPercent = @WarningThresholdPercent,
        @CriticalThresholdPercent = @CriticalThresholdPercent,
        @SendAlertEmail = @SendAlertEmail,
        @Debug = @Debug;
    
    -- Show summary of all collected data
    IF @Debug = 1
    BEGIN
        PRINT '';
        PRINT 'Overall Summary:';
        
        SELECT 
            COUNT(DISTINCT ServerName) AS TotalServers,
            COUNT(*) AS TotalDrives,
            SUM(CASE WHEN FreeSpacePercent < @CriticalThresholdPercent THEN 1 ELSE 0 END) AS CriticalDrives,
            SUM(CASE WHEN FreeSpacePercent < @WarningThresholdPercent 
                      AND FreeSpacePercent >= @CriticalThresholdPercent THEN 1 ELSE 0 END) AS WarningDrives,
            MIN(FreeSpacePercent) AS LowestFreePercent
        FROM dbo.DiskSpaceInventory;
    END
    
    -- Check for any critical alerts across all servers
    IF EXISTS (
        SELECT 1 FROM dbo.DiskSpaceInventory 
        WHERE FreeSpacePercent < @CriticalThresholdPercent
    )
    BEGIN
        DECLARE @EmailBody NVARCHAR(MAX) = 'CRITICAL DISK SPACE ALERTS:' + CHAR(13) + CHAR(10) + CHAR(13) + CHAR(10);
        
        SELECT @EmailBody = @EmailBody + 
            '[CRITICAL] ' + ServerName + ' ' + DriveLetter + ': - ' + 
            CAST(FreeSpaceGB AS VARCHAR) + ' GB free (' + 
            CAST(FreeSpacePercent AS VARCHAR) + '%)' + CHAR(13) + CHAR(10)
        FROM dbo.DiskSpaceInventory
        WHERE FreeSpacePercent < @CriticalThresholdPercent
        ORDER BY FreeSpacePercent ASC;
        
        IF @SendAlertEmail = 1
        BEGIN
            EXEC msdb.dbo.sp_send_dbmail
                @profile_name = 'DBAOpsProfile',
                @recipients = 'DBAOps@YOURORG.gov; SQLServerAdminNotification@YOURORG.gov',
                @subject = '*** CRITICAL: Disk Space Below 10% ***',
                @body = @EmailBody,
                @body_format = 'TEXT';
        END
    END
END;

GO

-- [dbo].[usp_CollectDiskSpace_AllServers]
CREATE OR ALTER PROCEDURE dbo.usp_CollectDiskSpace_AllServers
    @Debug BIT = 0
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @ServerName NVARCHAR(256);
    DECLARE @SQL NVARCHAR(MAX);
    DECLARE @ServersChecked INT = 0;
    DECLARE @ServersSucceeded INT = 0;
    DECLARE @ServersFailed INT = 0;
    
    -- Create temp table for results
    CREATE TABLE #DiskResults (
        ServerName VARCHAR(256),
        DriveLetter CHAR(1),
        VolumeName NVARCHAR(256),
        TotalSizeGB DECIMAL(18,2),
        FreeSpaceGB DECIMAL(18,2),
        FreeSpacePercent DECIMAL(5,2)
    );
    
    -- Get servers from CMS
    DECLARE server_cursor CURSOR FOR
        SELECT DISTINCT s.server_name
        FROM msdb.dbo.sysmanagement_shared_registered_servers_internal s
        WHERE s.server_name IS NOT NULL
          AND s.server_name <> ''
        ORDER BY s.server_name;
    
    OPEN server_cursor;
    FETCH NEXT FROM server_cursor INTO @ServerName;
    
    WHILE @@FETCH_STATUS = 0
    BEGIN
        SET @ServersChecked = @ServersChecked + 1;
        
        BEGIN TRY
            IF EXISTS (SELECT 1 FROM sys.servers WHERE name = @ServerName)
            BEGIN
                SET @SQL = '
                INSERT INTO #DiskResults
                SELECT 
                    ''' + @ServerName + ''',
                    LEFT(vs.volume_mount_point, 1),
                    vs.logical_volume_name,
                    CAST(vs.total_bytes / 1073741824.0 AS DECIMAL(18,2)),
                    CAST(vs.available_bytes / 1073741824.0 AS DECIMAL(18,2)),
                    CAST(vs.available_bytes * 100.0 / NULLIF(vs.total_bytes, 0) AS DECIMAL(5,2))
                FROM [' + @ServerName + '].master.sys.master_files mf
                CROSS APPLY [' + @ServerName + '].master.sys.dm_os_volume_stats(mf.database_id, mf.file_id) vs
                GROUP BY vs.volume_mount_point, vs.logical_volume_name, vs.total_bytes, vs.available_bytes';
                
                EXEC sp_executesql @SQL;
                SET @ServersSucceeded = @ServersSucceeded + 1;
                
                IF @Debug = 1
                    PRINT 'SUCCESS: ' + @ServerName;
            END
            ELSE
            BEGIN
                IF @Debug = 1
                    PRINT 'NO LINKED SERVER: ' + @ServerName;
                SET @ServersFailed = @ServersFailed + 1;
            END
        END TRY
        BEGIN CATCH
            SET @ServersFailed = @ServersFailed + 1;
            IF @Debug = 1
                PRINT 'ERROR: ' + @ServerName + ' - ' + ERROR_MESSAGE();
        END CATCH
        
        FETCH NEXT FROM server_cursor INTO @ServerName;
    END
    
    CLOSE server_cursor;
    DEALLOCATE server_cursor;
    
    -- Also collect from local server (only if not already collected via CMS)
    IF NOT EXISTS (SELECT 1 FROM #DiskResults WHERE ServerName = @@SERVERNAME)
    BEGIN
        INSERT INTO #DiskResults
        SELECT 
            @@SERVERNAME,
            LEFT(vs.volume_mount_point, 1),
            vs.logical_volume_name,
            CAST(vs.total_bytes / 1073741824.0 AS DECIMAL(18,2)),
            CAST(vs.available_bytes / 1073741824.0 AS DECIMAL(18,2)),
            CAST(vs.available_bytes * 100.0 / NULLIF(vs.total_bytes, 0) AS DECIMAL(5,2))
        FROM sys.master_files mf
        CROSS APPLY sys.dm_os_volume_stats(mf.database_id, mf.file_id) vs
        GROUP BY vs.volume_mount_point, vs.logical_volume_name, vs.total_bytes, vs.available_bytes;
    END
    
    -- MERGE results into main table using deduplicated source
    ;WITH DeduplicatedSource AS (
        SELECT 
            ServerName,
            DriveLetter,
            VolumeName,
            TotalSizeGB,
            FreeSpaceGB,
            FreeSpacePercent,
            ROW_NUMBER() OVER (
                PARTITION BY ServerName, DriveLetter 
                ORDER BY TotalSizeGB DESC
            ) AS rn
        FROM #DiskResults
    )
    MERGE dbo.DiskSpaceInventory AS Target
    USING (SELECT * FROM DeduplicatedSource WHERE rn = 1) AS Source
    ON Target.ServerName = Source.ServerName 
       AND Target.DriveLetter = Source.DriveLetter
    WHEN MATCHED THEN
        UPDATE SET 
            VolumeName = Source.VolumeName,
            TotalSizeGB = Source.TotalSizeGB,
            FreeSpaceGB = Source.FreeSpaceGB,
            FreeSpacePercent = Source.FreeSpacePercent,
            CollectedAt = SYSDATETIME()
    WHEN NOT MATCHED THEN
        INSERT (ServerName, DriveLetter, VolumeName, TotalSizeGB, FreeSpaceGB, FreeSpacePercent)
        VALUES (Source.ServerName, Source.DriveLetter, Source.VolumeName, 
                Source.TotalSizeGB, Source.FreeSpaceGB, Source.FreeSpacePercent);
    
    DROP TABLE #DiskResults;
    
    -- Summary
    PRINT '';
    PRINT 'Collection Summary:';
    PRINT '  Servers in CMS: ' + CAST(@ServersChecked AS VARCHAR);
    PRINT '  Succeeded: ' + CAST(@ServersSucceeded AS VARCHAR);
    PRINT '  Failed: ' + CAST(@ServersFailed AS VARCHAR);
    
    -- Return summary
    SELECT 
        @ServersChecked AS CMSServers,
        @ServersSucceeded AS Succeeded,
        @ServersFailed AS Failed,
        COUNT(DISTINCT ServerName) AS ServersWithData,
        COUNT(*) AS TotalDrives,
        SUM(CASE WHEN FreeSpacePercent < 10 THEN 1 ELSE 0 END) AS CriticalDrives,
        SUM(CASE WHEN FreeSpacePercent < 20 AND FreeSpacePercent >= 10 THEN 1 ELSE 0 END) AS WarningDrives
    FROM dbo.DiskSpaceInventory;
END;

GO

-- [dbo].[usp_CollectJobDurationHistory]
CREATE OR ALTER PROCEDURE dbo.usp_CollectJobDurationHistory 
    @DaysBack INT = 7
AS
BEGIN
    SET NOCOUNT ON;
    DECLARE @ServerName NVARCHAR(128) = @@SERVERNAME;
    
    INSERT INTO dbo.JobDurationHistory (ServerName, JobName, RunDateTime, DurationSeconds, RunStatus)
    SELECT @ServerName, j.name, msdb.dbo.agent_datetime(jh.run_date, jh.run_time),
        (jh.run_duration/10000)*3600 + ((jh.run_duration%10000)/100)*60 + (jh.run_duration%100), jh.run_status
    FROM msdb.dbo.sysjobhistory jh INNER JOIN msdb.dbo.sysjobs j ON jh.job_id = j.job_id
    WHERE jh.step_id = 0 AND msdb.dbo.agent_datetime(jh.run_date, jh.run_time) >= DATEADD(DAY, -@DaysBack, GETDATE())
    AND NOT EXISTS (SELECT 1 FROM dbo.JobDurationHistory jdh WHERE jdh.ServerName = @ServerName AND jdh.JobName = j.name AND jdh.RunDateTime = msdb.dbo.agent_datetime(jh.run_date, jh.run_time));
END

GO

-- [dbo].[usp_CollectJobFailures]
CREATE OR ALTER PROCEDURE dbo.usp_CollectJobFailures 
    @DaysBack INT = 7
AS
BEGIN
    SET NOCOUNT ON;
    DECLARE @ServerName NVARCHAR(128) = @@SERVERNAME;
    
    INSERT INTO dbo.JobFailureHistory (ServerName, JobName, StepName, FailedAt, ErrorMessage)
    SELECT @ServerName, j.name, jh.step_name, msdb.dbo.agent_datetime(jh.run_date, jh.run_time), jh.message
    FROM msdb.dbo.sysjobhistory jh INNER JOIN msdb.dbo.sysjobs j ON jh.job_id = j.job_id
    WHERE jh.run_status = 0 AND msdb.dbo.agent_datetime(jh.run_date, jh.run_time) >= DATEADD(DAY, -@DaysBack, GETDATE())
    AND NOT EXISTS (SELECT 1 FROM dbo.JobFailureHistory jfh WHERE jfh.ServerName = @ServerName AND jfh.JobName = j.name AND jfh.FailedAt = msdb.dbo.agent_datetime(jh.run_date, jh.run_time));
END

GO

-- [dbo].[usp_CollectLocalDiskSpace]

-- This procedure can be deployed to all servers and called via CMS
CREATE   PROCEDURE dbo.usp_CollectLocalDiskSpace
AS
BEGIN
    SET NOCOUNT ON;
    
    ;WITH DiskData AS (
        SELECT 
            @@SERVERNAME AS ServerName,
            LEFT(vs.volume_mount_point, 1) AS DriveLetter,
            vs.logical_volume_name AS VolumeName,
            CAST(vs.total_bytes / 1073741824.0 AS DECIMAL(18,2)) AS TotalSizeGB,
            CAST(vs.available_bytes / 1073741824.0 AS DECIMAL(18,2)) AS FreeSpaceGB,
            CAST(vs.available_bytes * 100.0 / NULLIF(vs.total_bytes, 0) AS DECIMAL(5,2)) AS FreeSpacePercent
        FROM sys.master_files mf
        CROSS APPLY sys.dm_os_volume_stats(mf.database_id, mf.file_id) vs
        GROUP BY vs.volume_mount_point, vs.logical_volume_name, vs.total_bytes, vs.available_bytes
    )
    MERGE dbo.DiskSpaceInventory AS Target
    USING DiskData AS Source
    ON Target.ServerName = Source.ServerName 
       AND Target.DriveLetter = Source.DriveLetter
    WHEN MATCHED THEN
        UPDATE SET 
            VolumeName = Source.VolumeName,
            TotalSizeGB = Source.TotalSizeGB,
            FreeSpaceGB = Source.FreeSpaceGB,
            FreeSpacePercent = Source.FreeSpacePercent,
            CollectedAt = SYSDATETIME()
    WHEN NOT MATCHED THEN
        INSERT (ServerName, DriveLetter, VolumeName, TotalSizeGB, FreeSpaceGB, FreeSpacePercent)
        VALUES (Source.ServerName, Source.DriveLetter, Source.VolumeName, 
                Source.TotalSizeGB, Source.FreeSpaceGB, Source.FreeSpacePercent);
    
    -- Return collected data
    SELECT 
        ServerName,
        DriveLetter,
        TotalSizeGB,
        FreeSpaceGB,
        FreeSpacePercent,
        CASE 
            WHEN FreeSpacePercent < 10 THEN 'CRITICAL'
            WHEN FreeSpacePercent < 20 THEN 'WARNING'
            ELSE 'OK'
        END AS Status
    FROM dbo.DiskSpaceInventory
    WHERE ServerName = @@SERVERNAME;
END;

GO

-- [dbo].[usp_CollectMaintenanceLogs]

CREATE   PROCEDURE dbo.usp_CollectMaintenanceLogs
    @DaysBack INT = 7
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @Collected INT = 0;
    
    -- Collect from Ola Hallengren CommandLog if exists
    IF OBJECT_ID('dbo.CommandLog', 'U') IS NOT NULL
    BEGIN
        SELECT @Collected = COUNT(*) 
        FROM dbo.CommandLog 
        WHERE StartTime >= DATEADD(DAY, -@DaysBack, GETDATE());
    END
    
    -- Log the collection
    EXEC Logging.usp_WriteLog
        @LogLevel = 'INFO',
        @Category = 'Maintenance',
        @Operation = 'Collect Logs',
        @Message = 'Maintenance logs collection completed',
        @RowsAffected = @Collected;
    
    SELECT @Collected AS LogsCollected;
END

GO

-- [dbo].[usp_CollectServerConfigCompliance]

CREATE   PROCEDURE dbo.usp_CollectServerConfigCompliance
    @ServerName NVARCHAR(128) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    SELECT 
        cms.ServerName,
        COALESCE(cms.Environment, cms.CMSEnvironment, 'Unknown') AS Environment,
        cms.SQLVersion,
        cms.SQLEdition,
        cms.MaxDOP,
        cms.CostThreshold,
        cms.TotalMemoryGB,
        cms.CPUCount,
        cms.ComplianceTier
    FROM config.ServerInventory cms
    WHERE cms.IsActive = 1
    AND (cms.ServerType = 'SQL Server' OR cms.ServerType IS NULL)
    AND (@ServerName IS NULL OR cms.ServerName = @ServerName)
    ORDER BY cms.Environment, cms.ServerName;
END

GO

-- [dbo].[usp_CollectSQLServerInventory]

CREATE   PROCEDURE dbo.usp_CollectSQLServerInventory
AS
BEGIN
    SET NOCOUNT ON;
    
    -- This proc should collect from CMS (config.ServerInventory) 
    -- and populate other inventory tables as needed
    
    DECLARE @ServerList TABLE (
        ServerName NVARCHAR(128),
        Environment NVARCHAR(50),
        SQLVersion NVARCHAR(50)
    );
    
    -- Get server list from CMS
    INSERT INTO @ServerList (ServerName, Environment, SQLVersion)
    SELECT 
        ServerName,
        COALESCE(Environment, CMSEnvironment, 'Unknown'),
        SQLVersion
    FROM config.ServerInventory
    WHERE IsActive = 1
    AND (ServerType = 'SQL Server' OR ServerType IS NULL);
    
    -- Return the server list (actual collection logic would go here)
    SELECT * FROM @ServerList ORDER BY Environment, ServerName;
    
    PRINT 'Inventory collection initiated for ' + CAST(@@ROWCOUNT AS VARCHAR) + ' servers from CMS';
END

GO

-- [dbo].[usp_CreateDiskSpaceCollectorScript]

CREATE   PROCEDURE dbo.usp_CreateDiskSpaceCollectorScript
    @ServerName NVARCHAR(128) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    -- Generate disk space collection scripts for servers from CMS
    SELECT 
        cms.ServerName,
        COALESCE(cms.Environment, cms.CMSEnvironment, 'Unknown') AS Environment,
        'EXEC xp_fixeddrives;' AS CollectionScript
    FROM config.ServerInventory cms
    WHERE cms.IsActive = 1
    AND (cms.ServerType = 'SQL Server' OR cms.ServerType IS NULL)
    AND (@ServerName IS NULL OR cms.ServerName = @ServerName)
    ORDER BY cms.Environment, cms.ServerName;
END

GO

-- [dbo].[usp_DailyHealthCheck]

CREATE   PROCEDURE dbo.usp_DailyHealthCheck
AS
BEGIN
    SET NOCOUNT ON;
    
    -- Get all active SQL servers from CMS
    SELECT 
        cms.ServerName,
        COALESCE(cms.Environment, cms.CMSEnvironment, 'Unknown') AS Environment,
        cms.SQLVersion,
        cms.ComplianceTier,
        cms.LastSeen,
        CASE 
            WHEN h.ServerName IS NOT NULL THEN 'Online'
            WHEN cms.LastSeen >= DATEADD(DAY, -1, GETDATE()) THEN 'Recent'
            ELSE 'Check Required'
        END AS HealthStatus
    FROM config.ServerInventory cms
    LEFT JOIN (
        SELECT DISTINCT ServerName 
        FROM metrics.FACT_ServerHealth 
        WHERE CaptureTime >= DATEADD(HOUR, -2, GETDATE())
    ) h ON cms.ServerName = h.ServerName
    WHERE cms.IsActive = 1
    AND (cms.ServerType = 'SQL Server' OR cms.ServerType IS NULL)
    ORDER BY 
        CASE WHEN h.ServerName IS NULL THEN 1 ELSE 2 END,
        cms.Environment,
        cms.ServerName;
END

GO

-- [dbo].[usp_DBAOps_AddDashboardEntry]
CREATE OR ALTER PROCEDURE dbo.usp_DBAOps_AddDashboardEntry
(
    @ServerName     VARCHAR(200),
    @InstanceName   VARCHAR(200),
    @Category       VARCHAR(100),
    @MetricName     VARCHAR(200),
    @MetricValue    VARCHAR(200),
    @Status         VARCHAR(20),
    @Details        VARCHAR(MAX)
)
AS
BEGIN
    INSERT INTO dbo.DBAOps_ValidationDashboard
    (
        ServerName, InstanceName, Category,
        MetricName, MetricValue, Status, Details
    )
    VALUES
    (
        @ServerName, @InstanceName, @Category,
        @MetricName, @MetricValue, @Status, @Details
    );
END

GO

-- [dbo].[usp_DBAOps_CollectorPack]
CREATE OR ALTER PROCEDURE dbo.usp_DBAOps_CollectorPack
AS
BEGIN
    EXEC ctl.usp_Collect_BackupCompliance;
    EXEC ctl.usp_Collect_JobCompliance;
    EXEC ctl.usp_Collect_LoginCompliance;
    EXEC ctl.usp_Collect_ServiceCompliance;
END;

GO

-- [dbo].[usp_DBAOps_EnterpriseCollector]
CREATE OR ALTER PROCEDURE dbo.usp_DBAOps_EnterpriseCollector
AS
BEGIN
    SET NOCOUNT ON;
    DECLARE @StartTime DATETIME2 = SYSDATETIME();
    
    PRINT '================================================================================';
    PRINT ' DBAOps Enterprise Collector - Started: ' + CONVERT(VARCHAR, @StartTime, 120);
    PRINT '================================================================================';
    
    -- Performance
    BEGIN TRY EXEC perf.usp_CollectWaitStatistics; PRINT '   [OK] Wait Statistics'; END TRY BEGIN CATCH PRINT '   [WARN] Wait Statistics: ' + ERROR_MESSAGE(); END CATCH
    BEGIN TRY EXEC perf.usp_CollectTopQueries; PRINT '   [OK] Top Queries'; END TRY BEGIN CATCH PRINT '   [WARN] Top Queries: ' + ERROR_MESSAGE(); END CATCH
    BEGIN TRY EXEC perf.usp_CollectMissingIndexes; PRINT '   [OK] Missing Indexes'; END TRY BEGIN CATCH PRINT '   [WARN] Missing Indexes: ' + ERROR_MESSAGE(); END CATCH
    
    -- Capacity
    BEGIN TRY EXEC capacity.usp_CollectDiskSpace; PRINT '   [OK] Disk Space'; END TRY BEGIN CATCH PRINT '   [WARN] Disk Space: ' + ERROR_MESSAGE(); END CATCH
    BEGIN TRY EXEC capacity.usp_CollectDatabaseSizes; PRINT '   [OK] Database Sizes'; END TRY BEGIN CATCH PRINT '   [WARN] Database Sizes: ' + ERROR_MESSAGE(); END CATCH
    
    -- Backup (using bkp schema)
    BEGIN TRY EXEC bkp.usp_CollectBackupHistory; PRINT '   [OK] Backup History'; END TRY BEGIN CATCH PRINT '   [WARN] Backup History: ' + ERROR_MESSAGE(); END CATCH
    BEGIN TRY EXEC bkp.usp_EvaluateBackupCompliance; PRINT '   [OK] Backup Compliance'; END TRY BEGIN CATCH PRINT '   [WARN] Backup Compliance: ' + ERROR_MESSAGE(); END CATCH
    
    -- Security
    BEGIN TRY EXEC security.usp_CollectSysadminMembership; PRINT '   [OK] Sysadmin Membership'; END TRY BEGIN CATCH PRINT '   [WARN] Sysadmin: ' + ERROR_MESSAGE(); END CATCH
    BEGIN TRY EXEC security.usp_CollectTDEStatus; PRINT '   [OK] TDE Status'; END TRY BEGIN CATCH PRINT '   [WARN] TDE Status: ' + ERROR_MESSAGE(); END CATCH
    
    -- Replication
    BEGIN TRY EXEC repl.usp_CollectReplicationLatency; PRINT '   [OK] Replication'; END TRY BEGIN CATCH PRINT '   [WARN] Replication: ' + ERROR_MESSAGE(); END CATCH
    
    -- Jobs
    BEGIN TRY EXEC dbo.usp_CollectJobDurationHistory; PRINT '   [OK] Job Duration'; END TRY BEGIN CATCH PRINT '   [WARN] Job Duration: ' + ERROR_MESSAGE(); END CATCH
    BEGIN TRY EXEC dbo.usp_CollectJobFailures; PRINT '   [OK] Job Failures'; END TRY BEGIN CATCH PRINT '   [WARN] Job Failures: ' + ERROR_MESSAGE(); END CATCH
    
    PRINT '';
    PRINT ' Duration: ' + CAST(DATEDIFF(SECOND, @StartTime, SYSDATETIME()) AS VARCHAR) + ' seconds';
    PRINT '================================================================================';
END

GO

-- [dbo].[usp_DBAOps_EnvironmentAudit]
-- Fix: Complete Environment Audit Procedure (CMS as source of truth)
-- Run in SSMS on YOURSERVER01 against DBAOpsRepository

CREATE PROCEDURE dbo.usp_DBAOps_EnvironmentAudit
    @EmailReport BIT = 0,
    @EmailRecipients NVARCHAR(500) = NULL,
    @EmailProfile NVARCHAR(128) = 'DBAOpsProfile',
    @OutputToTable BIT = 0,
    @PrintHTML BIT = 0,
    @Debug BIT = 0
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @HTML NVARCHAR(MAX) = '';
    DECLARE @Subject NVARCHAR(255);
    DECLARE @TotalServers INT;
    DECLARE @ProdServers INT;
    DECLARE @NonProdServers INT;
    
    IF @Debug = 1
        PRINT '=== DBAOps Environment Audit - Started: ' + CONVERT(VARCHAR(30), GETDATE(), 120) + ' ===';
    
    -- =====================================================
    -- Get server counts from CMS - SOURCE OF TRUTH
    -- =====================================================
    SELECT @TotalServers = COUNT(*)
    FROM msdb.dbo.sysmanagement_shared_registered_servers_internal
    WHERE server_name NOT LIKE '%\%' AND server_name <> @@SERVERNAME;
    
    SELECT @ProdServers = COUNT(*)
    FROM msdb.dbo.sysmanagement_shared_registered_servers_internal cms
    LEFT JOIN config.ServerInventory si ON cms.server_name = si.ServerName
    WHERE cms.server_name NOT LIKE '%\%' AND cms.server_name <> @@SERVERNAME
      AND (si.Environment = 'Production' OR si.CMSEnvironment = 'Production' 
           OR cms.server_name NOT LIKE '%T' AND cms.server_name NOT LIKE '%DEV%');
    
    SET @NonProdServers = @TotalServers - @ProdServers;
    
    IF @Debug = 1
    BEGIN
        PRINT 'Total Servers (CMS): ' + CAST(@TotalServers AS VARCHAR(10));
        PRINT 'Production: ' + CAST(@ProdServers AS VARCHAR(10));
        PRINT 'Non-Production: ' + CAST(@NonProdServers AS VARCHAR(10));
    END
    
    -- =====================================================
    -- Build HTML Report
    -- =====================================================
    SET @Subject = 'DBAOps Daily Environment Audit - ' + FORMAT(GETDATE(), 'yyyy-MM-dd') + ' | ' + CAST(@TotalServers AS VARCHAR(10)) + ' Servers';
    
    SET @HTML = N'
<!DOCTYPE html>
<html>
<head>
    <style>
        body { font-family: Segoe UI, Arial, sans-serif; background-color: #f4f4f4; margin: 0; padding: 20px; }
        .container { max-width: 800px; margin: 0 auto; background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
        .header { background: linear-gradient(135deg, #1e3a5f 0%, #2d5a87 100%); color: white; padding: 20px; border-radius: 8px 8px 0 0; }
        .header h1 { margin: 0; font-size: 24px; }
        .header p { margin: 5px 0 0 0; opacity: 0.9; }
        .content { padding: 20px; }
        .summary-grid { display: flex; gap: 15px; margin-bottom: 20px; }
        .summary-card { flex: 1; background: #f8f9fa; border-radius: 8px; padding: 15px; text-align: center; border-left: 4px solid #2d5a87; }
        .summary-card .number { font-size: 32px; font-weight: bold; color: #1e3a5f; }
        .summary-card .label { color: #666; font-size: 12px; text-transform: uppercase; }
        table { width: 100%; border-collapse: collapse; margin-top: 15px; }
        th { background: #1e3a5f; color: white; padding: 12px; text-align: left; font-weight: 600; }
        td { padding: 10px 12px; border-bottom: 1px solid #eee; }
        tr:hover { background: #f8f9fa; }
        .section-title { color: #1e3a5f; font-size: 18px; margin: 25px 0 10px 0; padding-bottom: 5px; border-bottom: 2px solid #2d5a87; }
        .footer { background: #f8f9fa; padding: 15px 20px; text-align: center; color: #666; font-size: 12px; border-radius: 0 0 8px 8px; }
        .badge { display: inline-block; padding: 3px 8px; border-radius: 12px; font-size: 11px; font-weight: bold; }
        .badge-success { background: #d4edda; color: #155724; }
        .badge-warning { background: #fff3cd; color: #856404; }
        .badge-danger { background: #f8d7da; color: #721c24; }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>ðŸ–¥ï¸ DBAOps Daily Environment Audit</h1>
            <p>Report Generated: ' + FORMAT(GETDATE(), 'MMMM dd, yyyy h:mm tt') + '</p>
        </div>
        <div class="content">
            <div class="summary-grid">
                <div class="summary-card">
                    <div class="number">' + CAST(@TotalServers AS VARCHAR(10)) + '</div>
                    <div class="label">Total Servers</div>
                </div>
                <div class="summary-card">
                    <div class="number">' + CAST(@ProdServers AS VARCHAR(10)) + '</div>
                    <div class="label">Production</div>
                </div>
                <div class="summary-card">
                    <div class="number">' + CAST(@NonProdServers AS VARCHAR(10)) + '</div>
                    <div class="label">Non-Production</div>
                </div>
            </div>
            
            <h3 class="section-title">ðŸ“Š SQL Server Version Distribution</h3>
            <table>
                <tr><th>SQL Version</th><th>Server Count</th><th>Percentage</th></tr>';
    
    -- Add version distribution
    DECLARE @VersionHTML NVARCHAR(MAX) = '';
    
    SELECT @VersionHTML = @VersionHTML + 
        '<tr><td>' + SQLVersion + '</td><td>' + CAST(ServerCount AS VARCHAR(10)) + '</td><td>' + 
        CAST(CAST(ServerCount * 100.0 / @TotalServers AS DECIMAL(5,1)) AS VARCHAR(10)) + '%</td></tr>'
    FROM (
        SELECT 
            CASE 
                WHEN si.SQLVersion LIKE '17.%' THEN 'SQL Server 2025'
                WHEN si.SQLVersion LIKE '16.%' THEN 'SQL Server 2022'
                WHEN si.SQLVersion LIKE '15.%' THEN 'SQL Server 2019'
                WHEN si.SQLVersion LIKE '14.%' THEN 'SQL Server 2017'
                WHEN si.SQLVersion LIKE '13.%' THEN 'SQL Server 2016'
                WHEN si.SQLVersion LIKE '12.%' THEN 'SQL Server 2014'
                WHEN si.SQLVersion LIKE '11.%' THEN 'SQL Server 2012'
                ELSE 'Unknown/Not Collected'
            END AS SQLVersion,
            COUNT(*) AS ServerCount
        FROM msdb.dbo.sysmanagement_shared_registered_servers_internal cms
        LEFT JOIN config.ServerInventory si ON cms.server_name = si.ServerName
        WHERE cms.server_name NOT LIKE '%\%' AND cms.server_name <> @@SERVERNAME
        GROUP BY 
            CASE 
                WHEN si.SQLVersion LIKE '17.%' THEN 'SQL Server 2025'
                WHEN si.SQLVersion LIKE '16.%' THEN 'SQL Server 2022'
                WHEN si.SQLVersion LIKE '15.%' THEN 'SQL Server 2019'
                WHEN si.SQLVersion LIKE '14.%' THEN 'SQL Server 2017'
                WHEN si.SQLVersion LIKE '13.%' THEN 'SQL Server 2016'
                WHEN si.SQLVersion LIKE '12.%' THEN 'SQL Server 2014'
                WHEN si.SQLVersion LIKE '11.%' THEN 'SQL Server 2012'
                ELSE 'Unknown/Not Collected'
            END
    ) v
    ORDER BY ServerCount DESC;
    
    SET @HTML = @HTML + @VersionHTML + '</table>';
    
    -- Add Environment Distribution
    SET @HTML = @HTML + '
            <h3 class="section-title">ðŸ¢ Environment Distribution</h3>
            <table>
                <tr><th>Environment</th><th>Server Count</th></tr>';
    
    DECLARE @EnvHTML NVARCHAR(MAX) = '';
    
    SELECT @EnvHTML = @EnvHTML + 
        '<tr><td>' + Environment + '</td><td>' + CAST(ServerCount AS VARCHAR(10)) + '</td></tr>'
    FROM (
        SELECT 
            COALESCE(si.Environment, si.CMSEnvironment, 
                CASE 
                    WHEN cms.server_name LIKE '%PROD%' OR cms.server_name LIKE '%PRD%' THEN 'Production'
                    WHEN cms.server_name LIKE '%DEV%' THEN 'Development'
                    WHEN cms.server_name LIKE '%TEST%' OR cms.server_name LIKE '%T' THEN 'Test'
                    ELSE 'Production'
                END
            ) AS Environment,
            COUNT(*) AS ServerCount
        FROM msdb.dbo.sysmanagement_shared_registered_servers_internal cms
        LEFT JOIN config.ServerInventory si ON cms.server_name = si.ServerName
        WHERE cms.server_name NOT LIKE '%\%' AND cms.server_name <> @@SERVERNAME
        GROUP BY COALESCE(si.Environment, si.CMSEnvironment, 
                CASE 
                    WHEN cms.server_name LIKE '%PROD%' OR cms.server_name LIKE '%PRD%' THEN 'Production'
                    WHEN cms.server_name LIKE '%DEV%' THEN 'Development'
                    WHEN cms.server_name LIKE '%TEST%' OR cms.server_name LIKE '%T' THEN 'Test'
                    ELSE 'Production'
                END
            )
    ) e
    ORDER BY ServerCount DESC;
    
    SET @HTML = @HTML + @EnvHTML + '</table>';
    
    -- Add Compliance Tier Distribution
    SET @HTML = @HTML + '
            <h3 class="section-title">ðŸ”’ Compliance Tier Distribution</h3>
            <table>
                <tr><th>Compliance Tier</th><th>Server Count</th></tr>';
    
    DECLARE @TierHTML NVARCHAR(MAX) = '';
    
    SELECT @TierHTML = @TierHTML + 
        '<tr><td>' + ComplianceTier + '</td><td>' + CAST(ServerCount AS VARCHAR(10)) + '</td></tr>'
    FROM (
        SELECT 
            COALESCE(si.ComplianceTier, 'Unclassified') AS ComplianceTier,
            COUNT(*) AS ServerCount
        FROM msdb.dbo.sysmanagement_shared_registered_servers_internal cms
        LEFT JOIN config.ServerInventory si ON cms.server_name = si.ServerName
        WHERE cms.server_name NOT LIKE '%\%' AND cms.server_name <> @@SERVERNAME
        GROUP BY COALESCE(si.ComplianceTier, 'Unclassified')
    ) t
    ORDER BY ServerCount DESC;
    
    SET @HTML = @HTML + @TierHTML + '</table>';
    
    -- Close HTML
    SET @HTML = @HTML + '
        </div>
        <div class="footer">
            <p>DBAOpsâ„¢ Enterprise Platform | Generated by YOURSERVER01</p>
            <p>Source of Truth: Central Management Server (CMS) - ' + CAST(@TotalServers AS VARCHAR(10)) + ' registered servers</p>
        </div>
    </div>
</body>
</html>';
    
    -- =====================================================
    -- Output options
    -- =====================================================
    IF @PrintHTML = 1
        PRINT @HTML;
    
    IF @EmailReport = 1 AND @EmailRecipients IS NOT NULL
    BEGIN
        IF @Debug = 1
            PRINT 'Sending email to: ' + @EmailRecipients;
        
        EXEC msdb.dbo.sp_send_dbmail
            @profile_name = @EmailProfile,
            @recipients = @EmailRecipients,
            @subject = @Subject,
            @body = @HTML,
            @body_format = 'HTML';
        
        IF @Debug = 1
            PRINT 'Email sent successfully';
    END
    
    -- Return summary data
    SELECT 
        @TotalServers AS TotalServers,
        @ProdServers AS ProductionServers,
        @NonProdServers AS NonProductionServers,
        'CMS (msdb.dbo.sysmanagement_shared_registered_servers_internal)' AS SourceOfTruth,
        GETDATE() AS ReportGeneratedAt;
    
    IF @Debug = 1
        PRINT '=== Environment Audit Complete ===';
END

GO

-- [dbo].[usp_DBAOps_PurgeDashboard]
-- Fix the purge procedure - change CollectionDate to ValidationDate
CREATE PROCEDURE dbo.usp_DBAOps_PurgeDashboard
AS
BEGIN
    DELETE FROM dbo.DBAOps_ValidationDashboard
    WHERE ValidationDate < DATEADD(DAY, -14, GETDATE());
END

GO

-- [dbo].[usp_DBAOps_UniversalHealthCheck]

CREATE   PROCEDURE dbo.usp_DBAOps_UniversalHealthCheck
    @Verbose BIT = 0
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @RunID UNIQUEIDENTIFIER = NEWID();
    DECLARE @StartTime DATETIME2 = SYSDATETIME();
    DECLARE @IssueCount INT = 0;
    DECLARE @TotalChecks INT = 0;
    DECLARE @ServerCount INT = 0;
    DECLARE @AvgCPU DECIMAL(10,2) = 0;
    DECLARE @AvgMemory DECIMAL(10,2) = 0;
    DECLARE @FailedJobs INT = 0;
    DECLARE @BackupIssues INT = 0;
    DECLARE @LowDiskCount INT = 0;
    DECLARE @UnreachableCount INT = 0;
    DECLARE @ReplIssues INT = 0;
    DECLARE @OverallStatus NVARCHAR(20) = 'HEALTHY';
    DECLARE @HighCPUCount INT = 0;
    DECLARE @HighMemoryCount INT = 0;
    
    IF @Verbose = 1 PRINT 'Starting Universal Health Check at ' + CONVERT(VARCHAR(30), @StartTime, 120);
    
    SELECT @ServerCount = COUNT(*) FROM config.ServerInventory WHERE IsActive = 1;
    
    CREATE TABLE #HealthResults (CheckID INT IDENTITY(1,1), CheckName NVARCHAR(100), Status NVARCHAR(20), Details NVARCHAR(MAX));
    
    -- CHECK 1: CPU Health (multi-source)
    SET @TotalChecks = @TotalChecks + 1;
    IF OBJECT_ID('fact.ServerHealth', 'U') IS NOT NULL
        SELECT @AvgCPU = ISNULL(AVG(CPUPercent), 0), @HighCPUCount = ISNULL(SUM(CASE WHEN CPUPercent > 80 THEN 1 ELSE 0 END), 0)
        FROM fact.ServerHealth WHERE TimeCaptured >= DATEADD(MINUTE, -15, SYSDATETIME());
    IF @AvgCPU = 0 AND OBJECT_ID('DBAOps.ServerHealthTelemetry', 'U') IS NOT NULL
        SELECT @AvgCPU = ISNULL(AVG(CPUPercent), 0), @HighCPUCount = ISNULL(SUM(CASE WHEN CPUPercent > 80 THEN 1 ELSE 0 END), 0)
        FROM DBAOps.ServerHealthTelemetry WHERE CaptureTime >= DATEADD(MINUTE, -15, SYSDATETIME());
    IF @AvgCPU = 0 AND OBJECT_ID('DBAOps.PerformanceTelemetry', 'U') IS NOT NULL
        SELECT @AvgCPU = ISNULL(AVG(CPUPercent), 0), @HighCPUCount = ISNULL(SUM(CASE WHEN CPUPercent > 80 THEN 1 ELSE 0 END), 0)
        FROM DBAOps.PerformanceTelemetry WHERE CaptureTime >= DATEADD(MINUTE, -15, SYSDATETIME());
    INSERT INTO #HealthResults (CheckName, Status, Details) VALUES ('CPU Health', 
        CASE WHEN @HighCPUCount > 0 THEN 'WARNING' WHEN @AvgCPU = 0 THEN 'INFO' ELSE 'OK' END,
        CASE WHEN @AvgCPU = 0 THEN 'No recent CPU data' ELSE 'Avg: ' + CAST(@AvgCPU AS VARCHAR) + '%, High: ' + CAST(@HighCPUCount AS VARCHAR) END);
    IF @HighCPUCount > 0 SET @IssueCount = @IssueCount + 1;
    
    -- CHECK 2: Memory Health
    SET @TotalChecks = @TotalChecks + 1;
    IF OBJECT_ID('fact.ServerHealth', 'U') IS NOT NULL
        SELECT @AvgMemory = ISNULL(AVG(MemoryPercent), 0), @HighMemoryCount = ISNULL(SUM(CASE WHEN MemoryPercent > 85 THEN 1 ELSE 0 END), 0)
        FROM fact.ServerHealth WHERE TimeCaptured >= DATEADD(MINUTE, -15, SYSDATETIME());
    IF @AvgMemory = 0 AND OBJECT_ID('DBAOps.ServerHealthTelemetry', 'U') IS NOT NULL
        SELECT @AvgMemory = ISNULL(AVG(MemoryPercent), 0), @HighMemoryCount = ISNULL(SUM(CASE WHEN MemoryPercent > 85 THEN 1 ELSE 0 END), 0)
        FROM DBAOps.ServerHealthTelemetry WHERE CaptureTime >= DATEADD(MINUTE, -15, SYSDATETIME());
    IF @AvgMemory = 0 AND OBJECT_ID('DBAOps.PerformanceTelemetry', 'U') IS NOT NULL
        SELECT @AvgMemory = ISNULL(AVG(MemoryPercent), 0), @HighMemoryCount = ISNULL(SUM(CASE WHEN MemoryPercent > 85 THEN 1 ELSE 0 END), 0)
        FROM DBAOps.PerformanceTelemetry WHERE CaptureTime >= DATEADD(MINUTE, -15, SYSDATETIME());
    INSERT INTO #HealthResults (CheckName, Status, Details) VALUES ('Memory Health',
        CASE WHEN @HighMemoryCount > 0 THEN 'WARNING' WHEN @AvgMemory = 0 THEN 'INFO' ELSE 'OK' END,
        CASE WHEN @AvgMemory = 0 THEN 'No recent memory data' ELSE 'Avg: ' + CAST(@AvgMemory AS VARCHAR) + '%, High: ' + CAST(@HighMemoryCount AS VARCHAR) END);
    IF @HighMemoryCount > 0 SET @IssueCount = @IssueCount + 1;
    
    -- CHECK 3: Server Reachability (smart detection)
    SET @TotalChecks = @TotalChecks + 1;
    IF OBJECT_ID('config.ServerReachability', 'U') IS NOT NULL
        SELECT @UnreachableCount = ISNULL(COUNT(*), 0) FROM config.ServerReachability sr
        WHERE sr.Reachable = 0 AND sr.CheckedOn >= DATEADD(HOUR, -24, SYSDATETIME())
        AND EXISTS (SELECT 1 FROM config.ServerInventory si WHERE si.ServerName = sr.ServerName AND si.IsActive = 1);
    DECLARE @ReachStatus NVARCHAR(20) = CASE WHEN @UnreachableCount > (@ServerCount * 0.5) THEN 'WARNING'
        WHEN @UnreachableCount > 10 THEN 'CRITICAL' WHEN @UnreachableCount > 0 THEN 'WARNING' ELSE 'OK' END;
    INSERT INTO #HealthResults (CheckName, Status, Details) VALUES ('Server Reachability', @ReachStatus,
        CASE WHEN @UnreachableCount = 0 THEN 'All ' + CAST(@ServerCount AS VARCHAR) + ' servers reachable'
        WHEN @UnreachableCount > (@ServerCount * 0.5) THEN CAST(@UnreachableCount AS VARCHAR) + ' unreachable (possible collector issue)'
        ELSE CAST(@UnreachableCount AS VARCHAR) + ' servers unreachable' END);
    IF @UnreachableCount > 0 AND @UnreachableCount <= (@ServerCount * 0.5) SET @IssueCount = @IssueCount + 1;
    
    -- CHECK 4: Backup Compliance
    SET @TotalChecks = @TotalChecks + 1;
    IF OBJECT_ID('fact.BackupCompliance', 'U') IS NOT NULL
        SELECT @BackupIssues = ISNULL(COUNT(*), 0) FROM fact.BackupCompliance WHERE IsCompliant = 0 AND LoadDate >= DATEADD(DAY, -1, SYSDATETIME());
    INSERT INTO #HealthResults (CheckName, Status, Details) VALUES ('Backup Compliance',
        CASE WHEN @BackupIssues > 10 THEN 'CRITICAL' WHEN @BackupIssues > 0 THEN 'WARNING' ELSE 'OK' END,
        CASE WHEN @BackupIssues = 0 THEN 'All compliant' ELSE CAST(@BackupIssues AS VARCHAR) + ' non-compliant' END);
    IF @BackupIssues > 0 SET @IssueCount = @IssueCount + 1;
    
    -- CHECK 5: Job Failures
    SET @TotalChecks = @TotalChecks + 1;
    SELECT @FailedJobs = COUNT(DISTINCT j.name) FROM msdb.dbo.sysjobs j
    INNER JOIN msdb.dbo.sysjobhistory h ON j.job_id = h.job_id
    WHERE h.step_id = 0 AND h.run_status = 0 AND j.enabled = 1 AND j.name NOT LIKE '%Universal Health%'
    AND msdb.dbo.agent_datetime(h.run_date, h.run_time) >= DATEADD(HOUR, -24, GETDATE());
    INSERT INTO #HealthResults (CheckName, Status, Details) VALUES ('Job Failures',
        CASE WHEN @FailedJobs > 5 THEN 'CRITICAL' WHEN @FailedJobs > 0 THEN 'WARNING' ELSE 'OK' END,
        CASE WHEN @FailedJobs = 0 THEN 'No failures in 24h' ELSE CAST(@FailedJobs AS VARCHAR) + ' failed in 24h' END);
    IF @FailedJobs > 0 SET @IssueCount = @IssueCount + 1;
    
    -- CHECK 6: Disk Space
    SET @TotalChecks = @TotalChecks + 1;
    IF OBJECT_ID('DBAOps.DiskSpaceHistory', 'U') IS NOT NULL
        SELECT @LowDiskCount = ISNULL(COUNT(*), 0) FROM DBAOps.DiskSpaceHistory WHERE PercentFree < 10 AND CollectionTime >= DATEADD(HOUR, -4, SYSDATETIME());
    INSERT INTO #HealthResults (CheckName, Status, Details) VALUES ('Disk Space',
        CASE WHEN @LowDiskCount > 0 THEN 'WARNING' ELSE 'OK' END,
        CASE WHEN @LowDiskCount = 0 THEN 'All drives OK' ELSE CAST(@LowDiskCount AS VARCHAR) + ' drives <10% free' END);
    IF @LowDiskCount > 0 SET @IssueCount = @IssueCount + 1;
    
    -- CHECK 7: Replication Health (using IsHealthy)
    SET @TotalChecks = @TotalChecks + 1;
    IF OBJECT_ID('ctl.ReplicationHealth', 'U') IS NOT NULL
        SELECT @ReplIssues = ISNULL(COUNT(*), 0) FROM ctl.ReplicationHealth WHERE IsHealthy = 0 AND CollectionTime >= DATEADD(HOUR, -1, SYSDATETIME());
    INSERT INTO #HealthResults (CheckName, Status, Details) VALUES ('Replication Health',
        CASE WHEN @ReplIssues > 0 THEN 'WARNING' ELSE 'OK' END,
        CASE WHEN @ReplIssues = 0 THEN 'All healthy' ELSE CAST(@ReplIssues AS VARCHAR) + ' issues' END);
    IF @ReplIssues > 0 SET @IssueCount = @IssueCount + 1;
    
    -- Overall Status
    IF @BackupIssues > 10 OR @FailedJobs > 10 OR (@UnreachableCount > 10 AND @UnreachableCount <= (@ServerCount * 0.5))
        SET @OverallStatus = 'CRITICAL';
    ELSE IF @IssueCount > 2 OR @FailedJobs > 3 OR @LowDiskCount > 0 OR @ReplIssues > 0
        SET @OverallStatus = 'WARNING';
    ELSE
        SET @OverallStatus = 'HEALTHY';
    
    -- Log to summary
    IF OBJECT_ID('fact.HealthCheckSummary', 'U') IS NOT NULL
        INSERT INTO fact.HealthCheckSummary (RunID, RunTime, ServerName, OverallStatus, CpuLoadPercent, MemoryLoadPercent, BlockingCount, Notes, CreatedOn)
        VALUES (@RunID, @StartTime, @@SERVERNAME, @OverallStatus, @AvgCPU, @AvgMemory, 0,
            'Checks:' + CAST(@TotalChecks AS VARCHAR) + ' Issues:' + CAST(@IssueCount AS VARCHAR) + ' Unreachable:' + CAST(@UnreachableCount AS VARCHAR), SYSDATETIME());
    
    IF @Verbose = 1 SELECT CheckName, Status, Details FROM #HealthResults ORDER BY CheckID;
    
    SELECT @OverallStatus AS OverallStatus, @TotalChecks AS TotalChecks, @IssueCount AS IssuesFound, @ServerCount AS Servers,
           @AvgCPU AS AvgCPU, @AvgMemory AS AvgMemory, @UnreachableCount AS Unreachable, @FailedJobs AS FailedJobs,
           @BackupIssues AS BackupIssues, @LowDiskCount AS LowDisk, @ReplIssues AS ReplIssues;
    
    PRINT 'Universal Health Check Complete - Status: ' + @OverallStatus + ', Issues: ' + CAST(@IssueCount AS VARCHAR);
    DROP TABLE #HealthResults;
END;

GO

-- [dbo].[usp_DBAOps_UniversalHealthCheck_BACKUP]

CREATE PROCEDURE dbo.usp_DBAOps_UniversalHealthCheck
    @Verbose BIT = 0
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @StartTime DATETIME = GETDATE();
    DECLARE @IssueCount INT = 0;
    DECLARE @TotalChecks INT = 0;
    DECLARE @UnreachableCount INT = 0;
    DECLARE @BackupIssues INT = 0;
    DECLARE @FailedJobs INT = 0;
    DECLARE @LowDiskCount INT = 0;
    DECLARE @ReplIssues INT = 0;
    DECLARE @LoginFailures INT = 0;
    
    IF @Verbose = 1
        PRINT 'Starting Universal Health Check at ' + CONVERT(VARCHAR, @StartTime, 120);
    
    -- Create temp table for results
    CREATE TABLE #HealthResults (
        CheckID INT IDENTITY(1,1),
        CheckName NVARCHAR(100),
        Status NVARCHAR(20),
        Details NVARCHAR(MAX),
        CheckTime DATETIME DEFAULT GETDATE()
    );
    
    -- CHECK 1: Server Reachability
    -- Columns: CheckID, ServerName, Reachable, ErrorMessage, CheckedOn
    IF @Verbose = 1 PRINT 'Running check: Server Reachability';
    
    IF OBJECT_ID('config.ServerReachability', 'U') IS NOT NULL
    BEGIN
        SELECT @UnreachableCount = COUNT(*) 
        FROM config.ServerReachability 
        WHERE Reachable = 0 
        AND CheckedOn >= DATEADD(HOUR, -24, GETDATE());
    END
    
    IF @UnreachableCount > 0
    BEGIN
        INSERT INTO #HealthResults (CheckName, Status, Details)
        VALUES ('Server Reachability', 'WARNING', CAST(@UnreachableCount AS VARCHAR) + ' servers unreachable');
        SET @IssueCount = @IssueCount + 1;
    END
    ELSE
    BEGIN
        INSERT INTO #HealthResults (CheckName, Status, Details)
        VALUES ('Server Reachability', 'OK', 'All servers reachable');
    END
    SET @TotalChecks = @TotalChecks + 1;
    
    -- CHECK 2: Backup Compliance
    -- Columns: IsCompliant, CollectionTime
    IF @Verbose = 1 PRINT 'Running check: Backup Compliance';
    
    IF OBJECT_ID('ctl.BackupCompliance', 'U') IS NOT NULL
    BEGIN
        SELECT @BackupIssues = COUNT(*) 
        FROM ctl.BackupCompliance 
        WHERE IsCompliant = 0 
        AND CollectionTime >= DATEADD(DAY, -1, GETDATE());
    END
    
    IF @BackupIssues > 0
    BEGIN
        INSERT INTO #HealthResults (CheckName, Status, Details)
        VALUES ('Backup Compliance', 'CRITICAL', CAST(@BackupIssues AS VARCHAR) + ' databases non-compliant');
        SET @IssueCount = @IssueCount + 1;
    END
    ELSE
    BEGIN
        INSERT INTO #HealthResults (CheckName, Status, Details)
        VALUES ('Backup Compliance', 'OK', 'All databases compliant');
    END
    SET @TotalChecks = @TotalChecks + 1;
    
    -- CHECK 3: Job Failures
    IF @Verbose = 1 PRINT 'Running check: Job Failures';
    
    SELECT @FailedJobs = COUNT(DISTINCT j.name)
    FROM msdb.dbo.sysjobs j
    JOIN msdb.dbo.sysjobhistory h ON j.job_id = h.job_id
    WHERE h.step_id = 0
    AND h.run_status = 0
    AND h.run_date >= CONVERT(INT, CONVERT(VARCHAR(8), DATEADD(DAY, -1, GETDATE()), 112));
    
    IF @FailedJobs > 0
    BEGIN
        INSERT INTO #HealthResults (CheckName, Status, Details)
        VALUES ('Job Failures', 'WARNING', CAST(@FailedJobs AS VARCHAR) + ' jobs failed in 24h');
        SET @IssueCount = @IssueCount + 1;
    END
    ELSE
    BEGIN
        INSERT INTO #HealthResults (CheckName, Status, Details)
        VALUES ('Job Failures', 'OK', 'No failures in 24h');
    END
    SET @TotalChecks = @TotalChecks + 1;
    
    -- CHECK 4: Disk Space
    -- Columns: FreeGB, CollectedAt
    IF @Verbose = 1 PRINT 'Running check: Disk Space';
    
    IF OBJECT_ID('fact.DiskMetrics', 'U') IS NOT NULL
    BEGIN
        SELECT @LowDiskCount = COUNT(*) 
        FROM fact.DiskMetrics 
        WHERE FreeGB < 10 
        AND CollectedAt >= DATEADD(HOUR, -4, GETDATE());
    END
    
    IF @LowDiskCount > 0
    BEGIN
        INSERT INTO #HealthResults (CheckName, Status, Details)
        VALUES ('Disk Space', 'WARNING', CAST(@LowDiskCount AS VARCHAR) + ' drives low');
        SET @IssueCount = @IssueCount + 1;
    END
    ELSE
    BEGIN
        INSERT INTO #HealthResults (CheckName, Status, Details)
        VALUES ('Disk Space', 'OK', 'All drives OK');
    END
    SET @TotalChecks = @TotalChecks + 1;
    
    -- CHECK 5: Replication Health
    -- Columns: IsHealthy, CollectionTime
    IF @Verbose = 1 PRINT 'Running check: Replication Health';
    
    IF OBJECT_ID('ctl.ReplicationHealth', 'U') IS NOT NULL
    BEGIN
        SELECT @ReplIssues = COUNT(*) 
        FROM ctl.ReplicationHealth 
        WHERE IsHealthy = 0
        AND CollectionTime >= DATEADD(HOUR, -1, GETDATE());
    END
    
    IF @ReplIssues > 0
    BEGIN
        INSERT INTO #HealthResults (CheckName, Status, Details)
        VALUES ('Replication', 'WARNING', CAST(@ReplIssues AS VARCHAR) + ' issues');
        SET @IssueCount = @IssueCount + 1;
    END
    ELSE
    BEGIN
        INSERT INTO #HealthResults (CheckName, Status, Details)
        VALUES ('Replication', 'OK', 'Healthy');
    END
    SET @TotalChecks = @TotalChecks + 1;
    
    -- CHECK 6: Login Failures
    -- Columns: EventTime
    IF @Verbose = 1 PRINT 'Running check: Login Failures';
    
    IF OBJECT_ID('audit.LoginFailures', 'U') IS NOT NULL
    BEGIN
        SELECT @LoginFailures = COUNT(*) 
        FROM audit.LoginFailures 
        WHERE EventTime >= DATEADD(HOUR, -24, GETDATE());
    END
    
    IF @LoginFailures > 100
    BEGIN
        INSERT INTO #HealthResults (CheckName, Status, Details)
        VALUES ('Login Failures', 'WARNING', CAST(@LoginFailures AS VARCHAR) + ' failures (high)');
        SET @IssueCount = @IssueCount + 1;
    END
    ELSE
    BEGIN
        INSERT INTO #HealthResults (CheckName, Status, Details)
        VALUES ('Login Failures', 'OK', CAST(@LoginFailures AS VARCHAR) + ' failures (normal)');
    END
    SET @TotalChecks = @TotalChecks + 1;
    
    -- Log to fact.PerformanceMetrics
    INSERT INTO fact.PerformanceMetrics (ServerName, MetricName, MetricValue, MetricUnit, CollectedAt)
    VALUES 
        (@@SERVERNAME, 'HealthCheck_TotalChecks', @TotalChecks, 'Count', GETDATE()),
        (@@SERVERNAME, 'HealthCheck_IssuesFound', @IssueCount, 'Count', GETDATE());
    
    -- Output results if verbose
    IF @Verbose = 1
    BEGIN
        SELECT CheckName, Status, Details FROM #HealthResults ORDER BY CheckID;
    END
    
    -- Summary
    PRINT 'Universal Health Check Complete - Checks: ' + CAST(@TotalChecks AS VARCHAR) + ', Issues: ' + CAST(@IssueCount AS VARCHAR);
    
    DROP TABLE #HealthResults;
    RETURN @IssueCount;
END

GO

-- [dbo].[usp_DBAOps_UniversalHealthCheck_v2]
/********************************************************************************************
 Author:        John Bosco RHOYLLE INNOVATION EMPIRE™
 Title:         DBAOps Universal Health Check Engine (v2.0)
 Organization:  Your Organization (YOURORG)
 Role:          Senior SQL Server Database Administrator
 Created:       2025-12-04
 Purpose:       Enterprise-wide SQL Server Health Evaluation for 109 SQL Instances
 Compliance:    NIST 800-53, CJIS, DISA STIG SQL, DoD Cybersecurity Practices
 Description:   
   This stored procedure aggregates performance and health telemetry collected from all
   SQL Servers reporting into DBAOpsRepository. The procedure generates:
      - A new RunID for each execution
      - Summary record in fact.HealthCheckSummary
      - Detailed findings in fact.HealthCheckDetails 
      - Enterprise-wide scoring model for CPU, Memory, Disk, Backup, Jobs, Blocking, Waits

 Execution Notes:
   â€¢ Does NOT query remote servers â†’ reads only ingested telemetry
   â€¢ Safe for 109+ SQL servers
   â€¢ Designed for SQL Agent scheduling (15 min / 1 hour)
*********************************************************************************************/

CREATE   PROCEDURE dbo.usp_DBAOps_UniversalHealthCheck_v2
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @RunID UNIQUEIDENTIFIER = NEWID();
    DECLARE @Now DATETIME2 = SYSDATETIME();
    DECLARE @ServerCount INT = (SELECT COUNT(DISTINCT ServerName) FROM fact.ServerInventory);

    ------------------------------------------------------------------------------------------
    -- 1. Initialize Summary
    ------------------------------------------------------------------------------------------
    INSERT INTO fact.HealthCheckSummary
    (
        RunID, RunTime, ServerName, OverallStatus, CpuLoadPercent,
        MemoryLoadPercent, BlockingCount, WaitCategory, Notes, CreatedOn
    )
    SELECT
        @RunID,
        @Now,
        'DBAOpsRepository',
        'RUNNING',
        NULL, NULL, NULL, NULL,
        CONCAT('Enterprise Health Check Started for ', @ServerCount, ' servers'),
        @Now;

    ------------------------------------------------------------------------------------------
    -- 2. CPU Health Evaluation
    ------------------------------------------------------------------------------------------
    INSERT INTO fact.HealthCheckDetails (RunID, Category, Severity, Message, LoggedOn)
    SELECT 
        @RunID,
        'CPU',
        CASE WHEN SqlCpuPercent > 80 THEN 'HIGH'
             WHEN SqlCpuPercent > 60 THEN 'MEDIUM'
             ELSE 'LOW'
        END,
        CONCAT('CPU Load: ', SqlCpuPercent, '%. Server: ', ServerName),
        @Now
    FROM fact.PerformanceMetrics
    WHERE TimeCaptured >= DATEADD(MINUTE, -15, @Now);

    ------------------------------------------------------------------------------------------
    -- 3. Memory Health Evaluation
    ------------------------------------------------------------------------------------------
    INSERT INTO fact.HealthCheckDetails (RunID, Category, Severity, Message, LoggedOn)
    SELECT 
        @RunID,
        'MEMORY',
        CASE WHEN MemoryPercentUsed > 85 THEN 'HIGH'
             WHEN MemoryPercentUsed > 70 THEN 'MEDIUM'
             ELSE 'LOW'
        END,
        CONCAT('Memory Utilization: ', MemoryPercentUsed, '%. Server: ', ServerName),
        @Now
    FROM fact.PerformanceMetrics
    WHERE TimeCaptured >= DATEADD(MINUTE, -15, @Now);

    ------------------------------------------------------------------------------------------
    -- 4. Disk Space Issues
    ------------------------------------------------------------------------------------------
    INSERT INTO fact.HealthCheckDetails (RunID, Category, Severity, Message, LoggedOn)
    SELECT 
        @RunID,
        'DISK',
        CASE WHEN FreePercent < 10 THEN 'HIGH'
             WHEN FreePercent < 20 THEN 'MEDIUM'
             ELSE 'LOW'
        END,
        CONCAT(ServerName, ' Drive ', DriveLetter, ' low disk: ', FreePercent, '% free'),
        @Now
    FROM fact.DiskSpaceMetrics
    WHERE CaptureDate >= DATEADD(HOUR, -2, @Now);

    ------------------------------------------------------------------------------------------
    -- 5. Backup Compliance
    ------------------------------------------------------------------------------------------
    INSERT INTO fact.HealthCheckDetails (RunID, Category, Severity, Message, LoggedOn)
    SELECT
        @RunID,
        'BACKUP',
        CASE WHEN IsCompliant = 0 THEN 'HIGH' ELSE 'LOW' END,
        CONCAT('Backup Non-Compliance: ', ServerName, ' / ', DatabaseName, 
               ' - LastFull: ', FORMAT(LastFullBackup, 'yyyy-MM-dd HH:mm:ss')),
        @Now
    FROM fact.BackupMetrics;

    ------------------------------------------------------------------------------------------
    -- 6. Job Failures
    ------------------------------------------------------------------------------------------
    INSERT INTO fact.HealthCheckDetails (RunID, Category, Severity, Message, LoggedOn)
    SELECT
        @RunID,
        'JOB',
        'HIGH',
        CONCAT('Job Failure: ', ServerName, ' / ', JobName, 
               ' - Last Status: ', LastRunStatus),
        @Now
    FROM fact.JobMetrics
    WHERE LastRunStatus NOT IN ('Succeeded', 'Success');

    ------------------------------------------------------------------------------------------
    -- 7. Blocking Analysis
    ------------------------------------------------------------------------------------------
    INSERT INTO fact.HealthCheckDetails (RunID, Category, Severity, Message, LoggedOn)
    SELECT
        @RunID,
        'BLOCKING',
        CASE WHEN BlockDurationSeconds > 60 THEN 'HIGH' ELSE 'MEDIUM' END,
        CONCAT('Blocking Detected: Blocker ', BlockerSPID, 
               ' Duration: ', BlockDurationSeconds,' sec (',ServerName,')'),
        @Now
    FROM fact.BlockingMetrics;

    ------------------------------------------------------------------------------------------
    -- 8. Wait Statistics Trends
    ------------------------------------------------------------------------------------------
    INSERT INTO fact.HealthCheckDetails (RunID, Category, Severity, Message, LoggedOn)
    SELECT
        @RunID,
        'WAITS',
        CASE WHEN Delta_wait_time_ms > 5000 THEN 'HIGH' ELSE 'MEDIUM' END,
        CONCAT('High Wait Type: ', wait_type, 
               ' Delta(ms): ', Delta_wait_time_ms, ' Server: ', ServerName),
        @Now
    FROM fact.WaitStatsDelta
    WHERE ComputedOn >= DATEADD(HOUR, -1, @Now);

    ------------------------------------------------------------------------------------------
    -- 9. Update Summary with Aggregate Scores
    ------------------------------------------------------------------------------------------
    DECLARE @CpuAvg DECIMAL(10,2) =
        (SELECT AVG(SqlCpuPercent) FROM fact.PerformanceMetrics 
         WHERE TimeCaptured >= DATEADD(MINUTE, -15, @Now));

    DECLARE @MemAvg DECIMAL(10,2) =
        (SELECT AVG(MemoryPercentUsed) FROM fact.PerformanceMetrics 
         WHERE TimeCaptured >= DATEADD(MINUTE, -15, @Now));

    DECLARE @BlockingCount INT =
        (SELECT COUNT(*) FROM fact.BlockingMetrics
         WHERE Timestamp >= DATEADD(MINUTE, -30, @Now));

    DECLARE @TopWait VARCHAR(100) =
        (SELECT TOP 1 wait_type 
         FROM fact.WaitStatsDelta 
         WHERE ComputedOn >= DATEADD(HOUR, -1, @Now)
         ORDER BY Delta_wait_time_ms DESC);

    UPDATE fact.HealthCheckSummary
    SET CpuLoadPercent = @CpuAvg,
        MemoryLoadPercent = @MemAvg,
        BlockingCount = @BlockingCount,
        WaitCategory = @TopWait,
        OverallStatus =
            CASE 
                WHEN @CpuAvg > 80 OR @MemAvg > 85 OR @BlockingCount > 5 THEN 'CRITICAL'
                WHEN @CpuAvg > 60 OR @MemAvg > 70 THEN 'WARNING'
                ELSE 'HEALTHY'
            END,
        Notes = 'Completed successfully'
    WHERE RunID = @RunID;

END;

GO

-- [dbo].[usp_DBAOps_UniversalHealthCheck_vFinal]

/*******************************************************************************
  STEP 2: Create Universal Health Check Procedure with HTML Report
*******************************************************************************/
CREATE   PROCEDURE dbo.usp_DBAOps_UniversalHealthCheck_vFinal
    @SendReport BIT = 0,
    @ReportType VARCHAR(20) = 'SUMMARY' -- SUMMARY, CRITICAL_ONLY, FULL
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @RunID UNIQUEIDENTIFIER = NEWID();
    DECLARE @RunTime DATETIME2 = SYSDATETIME();
    DECLARE @ServerName SYSNAME = @@SERVERNAME;
    
    /*******************************************************************************
      COLLECT REAL-TIME METRICS FROM LOCAL DMVs
    *******************************************************************************/
    DECLARE 
        @Cpu DECIMAL(5,2) = 0,
        @MemUsedMB DECIMAL(10,2) = 0,
        @MemTotalMB DECIMAL(10,2) = 0,
        @Mem DECIMAL(5,2) = 0,
        @PLE INT = 0,
        @BlockingCount INT = 0,
        @ActiveSessions INT = 0,
        @WaitCategory VARCHAR(100),
        @OverallStatus VARCHAR(20);
    
    -- Get CPU from ring buffers
    SELECT @Cpu = 100 - AVG(record.value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]', 'int'))
    FROM (
        SELECT TOP 5 CAST(record AS XML) AS record
        FROM sys.dm_os_ring_buffers
        WHERE ring_buffer_type = N'RING_BUFFER_SCHEDULER_MONITOR'
        ORDER BY timestamp DESC
    ) x;
    SET @Cpu = ISNULL(@Cpu, 0);
    
    -- Get Memory
    SELECT 
        @MemTotalMB = total_physical_memory_kb / 1024.0,
        @MemUsedMB = (total_physical_memory_kb - available_physical_memory_kb) / 1024.0
    FROM sys.dm_os_sys_memory;
    SET @Mem = CASE WHEN @MemTotalMB > 0 THEN (@MemUsedMB / @MemTotalMB) * 100 ELSE 0 END;
    
    -- Get PLE
    SELECT @PLE = ISNULL(cntr_value, 0)
    FROM sys.dm_os_performance_counters
    WHERE counter_name = 'Page life expectancy' AND object_name LIKE '%Buffer Manager%';
    
    -- Get Blocking
    SELECT @BlockingCount = COUNT(*) FROM sys.dm_exec_requests WHERE blocking_session_id > 0;
    
    -- Get Active Sessions
    SELECT @ActiveSessions = COUNT(*) FROM sys.dm_exec_sessions WHERE is_user_process = 1 AND status = 'running';
    
    -- Get Top Wait
    SELECT TOP 1 @WaitCategory = wait_type
    FROM sys.dm_os_wait_stats
    WHERE wait_type NOT IN ('SLEEP_TASK','BROKER_TASK_STOP','BROKER_EVENTHANDLER','BROKER_RECEIVE_WAITFOR',
        'CHECKPOINT_QUEUE','CHKPT','CLR_AUTO_EVENT','CLR_MANUAL_EVENT','DISPATCHER_QUEUE_SEMAPHORE',
        'LAZYWRITER_SLEEP','LOGMGR_QUEUE','ONDEMAND_TASK_QUEUE','REQUEST_FOR_DEADLOCK_SEARCH',
        'RESOURCE_QUEUE','SERVER_IDLE_CHECK','SLEEP_BPOOL_FLUSH','SLEEP_SYSTEMTASK','SLEEP_TASK',
        'SP_SERVER_DIAGNOSTICS_SLEEP','SQLTRACE_BUFFER_FLUSH','WAITFOR','WAITFOR_TASKSHUTDOWN',
        'XE_DISPATCHER_WAIT','XE_TIMER_EVENT','HADR_FILESTREAM_IOMGR_IOCOMPLETION','HADR_WORK_QUEUE',
        'QDS_PERSIST_TASK_MAIN_LOOP_SLEEP','QDS_ASYNC_QUEUE','QDS_CLEANUP_STALE_QUERIES_TASK_MAIN_LOOP_SLEEP')
    ORDER BY wait_time_ms DESC;
    
    -- Determine Status
    SET @OverallStatus = CASE 
        WHEN @Cpu > 90 OR @Mem > 90 OR @PLE < 300 OR @BlockingCount > 5 THEN 'CRITICAL'
        WHEN @Cpu > 75 OR @Mem > 80 OR @PLE < 600 OR @BlockingCount > 0 THEN 'WARNING'
        ELSE 'HEALTHY'
    END;
    
    -- Log to summary table
    IF OBJECT_ID('fact.HealthCheckSummary', 'U') IS NOT NULL
    BEGIN
        INSERT INTO fact.HealthCheckSummary
        (RunID, RunTime, ServerName, OverallStatus, CpuLoadPercent, MemoryLoadPercent, BlockingCount, WaitCategory, Notes, CreatedOn)
        VALUES (@RunID, @RunTime, @ServerName, @OverallStatus, @Cpu, @Mem, @BlockingCount, @WaitCategory,
                CONCAT('PLE:', @PLE, ' Sessions:', @ActiveSessions), SYSDATETIME());
    END
    
    -- Log details
    IF OBJECT_ID('fact.HealthCheckDetails', 'U') IS NOT NULL
    BEGIN
        INSERT INTO fact.HealthCheckDetails (RunID, Category, Severity, Message, LoggedOn)
        VALUES
            (@RunID, 'CPU', CASE WHEN @Cpu > 90 THEN 'CRITICAL' WHEN @Cpu > 75 THEN 'WARNING' ELSE 'INFO' END,
             CONCAT('CPU: ', @Cpu, '% (NIST: 75% Warn, 90% Crit)'), SYSDATETIME()),
            (@RunID, 'MEMORY', CASE WHEN @Mem > 90 THEN 'CRITICAL' WHEN @Mem > 80 THEN 'WARNING' ELSE 'INFO' END,
             CONCAT('Memory: ', @Mem, '% (DISA: 80% Warn, 90% Crit)'), SYSDATETIME()),
            (@RunID, 'PLE', CASE WHEN @PLE < 300 THEN 'CRITICAL' WHEN @PLE < 600 THEN 'WARNING' ELSE 'INFO' END,
             CONCAT('PLE: ', @PLE, ' (Best Practice: 300 Crit, 600 Warn)'), SYSDATETIME()),
            (@RunID, 'BLOCKING', CASE WHEN @BlockingCount > 5 THEN 'CRITICAL' WHEN @BlockingCount > 0 THEN 'WARNING' ELSE 'INFO' END,
             CONCAT('Blocking: ', @BlockingCount, ' (CJIS: 0 Warn, 5 Crit)'), SYSDATETIME()),
            (@RunID, 'WAITS', 'INFO', CONCAT('Top Wait: ', ISNULL(@WaitCategory,'NONE')), SYSDATETIME());
    END
    
    -- Return results
    SELECT @RunID AS RunID, @ServerName AS ServerName, @OverallStatus AS Status,
           @Cpu AS CPU_Percent, @Mem AS Memory_Percent, @PLE AS PageLifeExpectancy,
           @BlockingCount AS BlockingSessions, @ActiveSessions AS ActiveSessions, @WaitCategory AS TopWait;
END;

GO

-- [dbo].[usp_GenerateDailySummary]

CREATE PROCEDURE dbo.usp_GenerateDailySummary 
AS 
BEGIN     
    SET NOCOUNT ON;      
    
    DECLARE @Today DATE = CONVERT(DATE, GETDATE());
    DECLARE @Now DATETIME = GETDATE();

    -----------------------------------------     
    -- 1. ALERT SUMMARY (Delete existing, then insert)
    -----------------------------------------
    DELETE FROM dbo.fact_DailyAlertSummary WHERE SummaryDate = @Today;
    
    INSERT INTO dbo.fact_DailyAlertSummary     
    (         
        SummaryDate, TotalAlerts, HighAlerts, MediumAlerts, LowAlerts, UniqueServers, ReportGeneratedOn     
    )     
    SELECT         
        @Today,         
        ISNULL(COUNT(*), 0),         
        ISNULL(SUM(CASE WHEN AlertSeverity = 'HIGH' THEN 1 ELSE 0 END), 0),         
        ISNULL(SUM(CASE WHEN AlertSeverity = 'MEDIUM' THEN 1 ELSE 0 END), 0),         
        ISNULL(SUM(CASE WHEN AlertSeverity = 'LOW' THEN 1 ELSE 0 END), 0),         
        ISNULL(COUNT(DISTINCT ServerName), 0),
        @Now     
    FROM dbo.fact_Alerts     
    WHERE CAST(AlertTime AS DATE) = @Today;

    -----------------------------------------     
    -- 2. PERFORMANCE SUMMARY (using dbo.fact_PerformanceSnapshot - 107 servers)
    -----------------------------------------
    DELETE FROM dbo.fact_DailyPerformanceSummary WHERE SummaryDate = @Today;
    
    INSERT INTO dbo.fact_DailyPerformanceSummary     
    (         
        SummaryDate, ServerName,         
        AvgCpuPercent, MaxCpuPercent,
        AvgMemoryPercent, MaxMemoryPercent,
        AvgDiskLatencyMs, MaxDiskLatencyMs,
        TotalBatchRequests, TotalCompilations,
        PageLifeExpectancy, BufferCacheHitRatio,
        AvgMemoryUsedMB, MaxMemoryUsedMB,         
        AvgBatchRequests, MaxBatchRequests,         
        AvgPLE, MinPLE,
        CollectionTime, InsertedAt
    )     
    SELECT         
        @Today,         
        ServerName,         
        ISNULL(AVG(CAST(SqlCpuPercent AS DECIMAL(10,2))), 0),         
        ISNULL(MAX(CAST(SqlCpuPercent AS DECIMAL(10,2))), 0),
        ISNULL(AVG(CASE WHEN TotalMemoryMB > 0 THEN (TotalMemoryMB - AvailableMemoryMB) * 100.0 / TotalMemoryMB ELSE 0 END), 0),
        ISNULL(MAX(CASE WHEN TotalMemoryMB > 0 THEN (TotalMemoryMB - AvailableMemoryMB) * 100.0 / TotalMemoryMB ELSE 0 END), 0),
        0, -- Disk latency not available in this table
        0,
        ISNULL(SUM(BatchRequestsPerSec), 0),
        0,
        ISNULL(AVG(PageLifeExpectancy), 0),
        0, -- Buffer cache hit ratio not available
        ISNULL(AVG(SqlMemoryUsedMB), 0),         
        ISNULL(MAX(SqlMemoryUsedMB), 0),         
        ISNULL(AVG(BatchRequestsPerSec), 0),         
        ISNULL(MAX(BatchRequestsPerSec), 0),         
        ISNULL(AVG(PageLifeExpectancy), 0),         
        ISNULL(MIN(PageLifeExpectancy), 0),
        @Now,
        @Now
    FROM dbo.fact_PerformanceSnapshot     
    WHERE CAST(SnapshotTime AS DATE) = @Today
    GROUP BY ServerName;       

    -----------------------------------------     
    -- 3. RESULT SETS FOR EMAIL     
    -----------------------------------------     
    SELECT * FROM dbo.fact_DailyAlertSummary WHERE SummaryDate = @Today;      
    SELECT * FROM dbo.fact_DailyPerformanceSummary WHERE SummaryDate = @Today;      
    SELECT * FROM dbo.fact_Alerts WHERE CAST(AlertTime AS DATE) = @Today ORDER BY AlertSeverity DESC;  
END

GO

-- [dbo].[usp_Get_BackupComplianceReport]

CREATE PROCEDURE dbo.usp_Get_BackupComplianceReport
    @Days INT = 7
AS
BEGIN
    SET NOCOUNT ON;
    
    -- Summary
    SELECT 
        COUNT(*) AS TotalDatabases,
        SUM(CASE WHEN IsCompliant = 1 THEN 1 ELSE 0 END) AS Compliant,
        SUM(CASE WHEN IsCompliant = 0 THEN 1 ELSE 0 END) AS NonCompliant,
        CAST(SUM(CASE WHEN IsCompliant = 1 THEN 1.0 ELSE 0 END) / COUNT(*) * 100 AS DECIMAL(5,2)) AS CompliancePercent
    FROM fact.BackupCompliance
    WHERE LastBackupTime >= DATEADD(DAY, -@Days, GETDATE());
    
    -- Non-compliant details
    SELECT TOP 50
        ServerName,
        DatabaseName,
        BackupType,
        LastBackupTime,
        DATEDIFF(HOUR, LastBackupTime, GETDATE()) AS HoursSinceBackup
    FROM fact.BackupCompliance
    WHERE IsCompliant = 0
    AND LastBackupTime >= DATEADD(DAY, -@Days, GETDATE())
    ORDER BY LastBackupTime;
END

GO

-- [dbo].[usp_Get_FrameworkStatus]

CREATE PROCEDURE dbo.usp_Get_FrameworkStatus
AS
BEGIN
    SET NOCOUNT ON;
    
    SELECT * FROM dbo.vw_FrameworkHealth_Dashboard;
    
    SELECT * FROM dbo.vw_JobStatus_Current
    WHERE LastStatus = 'Failed'
    ORDER BY LastRunTime DESC;
END

GO

-- [dbo].[usp_Get_ServerInventoryReport]

CREATE PROCEDURE dbo.usp_Get_ServerInventoryReport
    @Environment NVARCHAR(50) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    SELECT 
        ServerName,
        InstanceName,
        Environment,
        SqlBuild,
        SQLEdition,
        CPUCount,
        TotalMemoryMB,
        LastSeen,
        IsActive
    FROM config.ServerInventory
    WHERE (@Environment IS NULL OR Environment = @Environment)
    AND IsActive = 1
    ORDER BY Environment, ServerName;
END

GO

-- [dbo].[usp_Get_SQLServer_EndOfService]

-- Create stored procedure for detailed reporting
CREATE   PROCEDURE [dbo].[usp_Get_SQLServer_EndOfService]
    @OutputType VARCHAR(20) = 'ALL',  -- ALL, CRITICAL, WARNING, EOL, SUMMARY
    @Environment VARCHAR(50) = NULL    -- Filter by environment (optional)
AS
BEGIN
    SET NOCOUNT ON;

    IF @OutputType = 'SUMMARY'
    BEGIN
        SELECT 
            SQLVersionName,
            ServerCount,
            EndOfSupport,
            DaysUntilEOS,
            Status,
            SupportStatus
        FROM dbo.vw_SQLServer_EOS_Summary
        ORDER BY EndOfSupport;
        RETURN;
    END

    SELECT 
        ServerName,
        InstanceName,
        SQLVersion,
        SQLVersionMajor,
        SQLBuild,
        SQLCumulativeUpdate,
        Edition,
        EndOfSupport,
        DaysUntilEOS,
        UrgencyLevel,
        SupportStatus,
        RecommendedAction,
        LatestCU,
        LatestBuildNumber
    FROM dbo.vw_SQLServer_EOS_Status
    WHERE 
        (
            @OutputType = 'ALL'
            OR (@OutputType = 'CRITICAL' AND UrgencyLevel IN ('END OF LIFE', 'CRITICAL (<90 days)'))
            OR (@OutputType = 'WARNING' AND UrgencyLevel IN ('END OF LIFE', 'CRITICAL (<90 days)', 'HIGH (<180 days)', 'MEDIUM (<1 year)'))
            OR (@OutputType = 'EOL' AND UrgencyLevel = 'END OF LIFE')
        )
    ORDER BY 
        CASE UrgencyLevel
            WHEN 'END OF LIFE' THEN 1
            WHEN 'CRITICAL (<90 days)' THEN 2
            WHEN 'HIGH (<180 days)' THEN 3
            WHEN 'MEDIUM (<1 year)' THEN 4
            WHEN 'LOW (<2 years)' THEN 5
            ELSE 6
        END,
        DaysUntilEOS ASC,
        ServerName;
END

GO

-- [dbo].[usp_GetFailedJobs24h]

CREATE PROCEDURE dbo.usp_GetFailedJobs24h
AS
BEGIN
    SET NOCOUNT ON;
    
    IF OBJECT_ID('metrics.FACT_JobHistory', 'U') IS NOT NULL
    BEGIN
        SELECT TOP 50
            ServerName,
            JobName,
            LEFT(ErrorMessage, 200) AS ErrorMessage,
            MAX(RunDateTime) AS LastFailedTime,
            COUNT(*) AS FailureCount
        FROM metrics.FACT_JobHistory WITH (NOLOCK)
        WHERE RunStatus = 0
          AND StepID > 0
          AND RunDateTime >= DATEADD(HOUR, -24, GETDATE())
        GROUP BY ServerName, JobName, LEFT(ErrorMessage, 200)
        ORDER BY MAX(RunDateTime) DESC;
    END
END

GO

-- [dbo].[usp_GetTeamsHealthAdaptiveCard]

CREATE   PROCEDURE dbo.usp_GetTeamsHealthAdaptiveCard
AS
BEGIN
    SET NOCOUNT ON;

    -----------------------------------------------------------------
    -- Declare variables
    -----------------------------------------------------------------
    DECLARE @Rows NVARCHAR(MAX) = '';
    DECLARE @Card NVARCHAR(MAX);

    -----------------------------------------------------------------
    -- Pull Top 20 alerts from FACT_ServerHealth
    -----------------------------------------------------------------
    ;WITH Alerts AS (
        SELECT TOP 20
            ServerName,
            CPU_SignalWaitPct,
            RunnableTasksCount,
            TempDb_UsageMB,
            Memory_PLE,
            BlockingSessions,
            BackupAge_Log_Hours,
            CONVERT(NVARCHAR(19), CaptureTime, 120) AS CaptureTime
        FROM dbo.FACT_ServerHealth
        ORDER BY CPU_SignalWaitPct DESC
    )
    -----------------------------------------------------------------
    -- Build Adaptive Card rows
    -----------------------------------------------------------------
    SELECT @Rows =
        STRING_AGG(
            '{
                "type": "TableRow",
                "cells": [
                    { "type": "TableCell", "items": [{ "type": "TextBlock", "text": "' + ServerName + '" }] },
                    { "type": "TableCell", "items": [{ "type": "TextBlock", "text": "' + CAST(CPU_SignalWaitPct AS NVARCHAR(10)) + '%" }] },
                    { "type": "TableCell", "items": [{ "type": "TextBlock", "text": "' + CAST(RunnableTasksCount AS NVARCHAR(10)) + '" }] },
                    { "type": "TableCell", "items": [{ "type": "TextBlock", "text": "' + CAST(TempDb_UsageMB AS NVARCHAR(10)) + ' MB" }] },
                    { "type": "TableCell", "items": [{ "type": "TextBlock", "text": "' + CAST(BlockingSessions AS NVARCHAR(10)) + '" }] },
                    { "type": "TableCell", "items": [{ "type": "TextBlock", "text": "' + CAST(BackupAge_Log_Hours AS NVARCHAR(10)) + ' hrs" }] },
                    { "type": "TableCell", "items": [{ "type": "TextBlock", "text": "' + CaptureTime + '" }] }
                ]
            }'
        , ','
    )
    FROM Alerts;

    IF @Rows IS NULL SET @Rows = '';

    -----------------------------------------------------------------
    -- Build final Adaptive Card JSON
    -----------------------------------------------------------------
    SET @Card =
    '{
      "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
      "type": "AdaptiveCard",
      "version": "1.4",
      "body": [
        {
          "type": "TextBlock",
          "text": "DBAOps â€“ SQL Server Health Alerts",
          "weight": "Bolder",
          "size": "Large",
          "color": "Attention"
        },
        {
          "type": "Table",
          "columns": [
            {"width": 120},
            {"width": 80},
            {"width": 80},
            {"width": 80},
            {"width": 80},
            {"width": 80},
            {"width": 140}
          ],
          "rows": [
            ' + @Rows + '
          ]
        }
      ]
    }';

    SELECT @Card AS AdaptiveCardPayload;
END

GO

-- [dbo].[usp_GetTeamsHealthAlertJson]

/*===============================================================
  PART 2 â€” STORED PROCEDURE: JSON ALERT (Simple)
===============================================================*/

CREATE   PROCEDURE dbo.usp_GetTeamsHealthAlertJson
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @json NVARCHAR(MAX);

    ;WITH Alerts AS (
        SELECT TOP 20
            ServerName,
            CPU_SignalWaitPct,
            RunnableTasksCount,
            TempDB_PUsageMB,
            Memory_PLE,
            Memory_GrantsPending,
            BlockingSessions,
            BackupAge_Log_Hours,
            CAST(InstanceVersion AS NVARCHAR(200)) AS InstanceVersion,
            CaptureTime
        FROM dbo.ServerHealth_Latest
        WHERE Severity = 'RED'
        ORDER BY CaptureTime DESC
    )
    SELECT @json = (
        SELECT
            'DBAOps SQL Server Critical Alerts' AS title,
            'Critical SQL Server health conditions detected.' AS text,
            (
                SELECT
                    ServerName   AS name,
                    CPU_SignalWaitPct AS SignalWait,
                    RunnableTasksCount,
                    TempDB_PUsageMB,
                    Memory_PLE,
                    Memory_GrantsPending,
                    BlockingSessions,
                    BackupAge_Log_Hours,
                    InstanceVersion,
                    CaptureTime
                FROM Alerts
                FOR JSON PATH
            ) AS facts
        FOR JSON PATH, WITHOUT_ARRAY_WRAPPER
    );

    SELECT @json AS JsonPayload;
END

GO

-- [dbo].[usp_Insert_ServerInventory]
CREATE OR ALTER PROCEDURE dbo.usp_Insert_ServerInventory
(
    @ServerName SYSNAME,
    @InstanceName VARCHAR(200),
    @DomainName VARCHAR(200),
    @FQDN VARCHAR(300),
    @SQLVersion VARCHAR(50),
    @SQLVersionMajor INT,
    @SQLBuild VARCHAR(50),
    @SQLCumulativeUpdate VARCHAR(50),
    @Edition VARCHAR(200),
    @Collation VARCHAR(200),
    @IsClustered SQL_VARIANT,
    @IsAGNode SQL_VARIANT,
    @AGRole VARCHAR(50),
    @CpuCount INT,
    @MaxMemoryMB INT,
    @TotalMemoryGB DECIMAL(10,2),
    @NumTempDBFiles INT,
    @MachineType VARCHAR(20),
    @WindowsPlatform VARCHAR(200),
    @WindowsVersion VARCHAR(200),
    @WindowsBuild VARCHAR(200),
    @InstanceStartTime DATETIME,
    @CollectionTime DATETIME
)
AS
BEGIN
    SET NOCOUNT ON;

    -- SAFELY convert bit-like values to 0/1
    DECLARE
        @IsClusteredBit BIT = CASE WHEN @IsClustered IN (1,'1','True','true') THEN 1 ELSE 0 END,
        @IsAGNodeBit BIT = CASE WHEN @IsAGNode IN (1,'1','True','true') THEN 1 ELSE 0 END;

    INSERT INTO dbo.ServerInventory
    (
        ServerName, InstanceName, DomainName, FQDN,
        SQLVersion, SQLVersionMajor, SQLBuild, SQLCumulativeUpdate,
        Edition, Collation, IsClustered, IsAGNode, AGRole,
        CpuCount, MaxMemoryMB, TotalMemoryGB, NumTempDBFiles,
        MachineType, WindowsPlatform, WindowsVersion, WindowsBuild,
        InstanceStartTime, CollectionTime
    )
    VALUES
    (
        @ServerName, @InstanceName, @DomainName, @FQDN,
        @SQLVersion, @SQLVersionMajor, @SQLBuild, @SQLCumulativeUpdate,
        @Edition, @Collation, @IsClusteredBit, @IsAGNodeBit, @AGRole,
        @CpuCount, @MaxMemoryMB, @TotalMemoryGB, @NumTempDBFiles,
        @MachineType, @WindowsPlatform, @WindowsVersion, @WindowsBuild,
        @InstanceStartTime, @CollectionTime
    );
END

GO

-- [dbo].[usp_InsertBlockingData]

CREATE PROCEDURE dbo.usp_InsertBlockingData
    @ServerName SYSNAME,
    @BlockedSpid INT,
    @BlockingSpid INT,
    @DatabaseName NVARCHAR(128),
    @WaitType NVARCHAR(128),
    @DurationSeconds BIGINT
AS
BEGIN
    INSERT INTO dbo.BlockingSessions
    (
        ServerName,
        BlockedSpid,
        BlockingSpid,
        DatabaseName,
        WaitType,
        DurationSeconds
    )
    VALUES
    (
        @ServerName,
        @BlockedSpid,
        @BlockingSpid,
        @DatabaseName,
        @WaitType,
        @DurationSeconds
    );
END;

GO

-- [dbo].[usp_JobIntegrity_SendAlert]

CREATE PROCEDURE dbo.usp_JobIntegrity_SendAlert
AS
BEGIN
    SET NOCOUNT ON;

    IF EXISTS (
        SELECT 1 
        FROM dbo.JobIntegrityAudit
        WHERE CaptureDate >= DATEADD(MINUTE, -30, GETDATE())
          AND IssueDetected <> 'Healthy'
    )
    BEGIN
        EXEC msdb.dbo.sp_send_dbmail
            @profile_name = 'DBAOpsProfile',
            @recipients = 'SQLServerAdminNotification@YOURORG.gov',
            @subject = 'DBAOps Alert â€“ Job Integrity Issues Detected',
            @body = 'One or more SQL Agent job integrity problems were detected in the last scan.';
    END
END

GO

-- [dbo].[usp_JobIntegrity_UnhealthyAlert]

CREATE PROCEDURE dbo.usp_JobIntegrity_UnhealthyAlert
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @Now DATETIME = GETDATE();

    ;WITH CurrentIssues AS (
        SELECT 
            SQLInstance, JobName, IssueDetected, CaptureDate
        FROM dbo.vJobIntegrityDashboard
        WHERE IssueDetected <> 'Healthy'
    )
    SELECT * INTO #NewIssues FROM CurrentIssues;

    -- Identify issues NOT previously alerted
    ;WITH Diff AS (
        SELECT n.*
        FROM #NewIssues n
        LEFT JOIN dbo.JobIntegrityAlerts a
            ON n.SQLInstance = a.SQLInstance
            AND n.JobName = a.JobName
            AND n.IssueDetected = a.IssueDetected
        WHERE a.AlertID IS NULL
    )
    INSERT INTO dbo.JobIntegrityAlerts (SQLInstance, JobName, IssueDetected)
    SELECT SQLInstance, JobName, IssueDetected
    FROM Diff;

    -- Prepare HTML body for email
    DECLARE @Body NVARCHAR(MAX);

    SELECT @Body = (
        SELECT
            '<tr><td>' + SQLInstance + '</td><td>' + JobName + '</td><td>' 
            + IssueDetected + '</td></tr>'
        FROM dbo.JobIntegrityAlerts
        WHERE DATEDIFF(MINUTE, FirstDetected, @Now) < 5
        FOR XML PATH(''), TYPE
    ).value('.', 'NVARCHAR(MAX)');

    IF @Body IS NOT NULL AND LEN(@Body) > 0
    BEGIN
        SET @Body = 
        '<html><body><h3>DBAOps Job Integrity Alert</h3>
         <table border="1" cellpadding="4">
         <tr><th>SQL Instance</th><th>Job Name</th><th>Issue</th></tr>' 
         + @Body + 
         '</table></body></html>';

        EXEC msdb.dbo.sp_send_dbmail
            @profile_name = 'DBAOpsProfile',
            @recipients = 'SQLServerAdminNotification@YOURORG.gov',
            @subject = 'âš  SQL Agent Job Integrity Alert â€“ Unhealthy Jobs Detected',
            @body = @Body,
            @body_format = 'HTML';
    END;
END

GO

-- [dbo].[usp_JobIntegrityAudit_Cleanup]

CREATE   PROCEDURE dbo.usp_JobIntegrityAudit_Cleanup
(
    @RetentionDays INT = 60
)
AS
BEGIN
    SET NOCOUNT ON;

    DELETE FROM dbo.JobIntegrityAudit
    WHERE CaptureDate < DATEADD(DAY, -@RetentionDays, GETDATE());

    PRINT CONCAT('Cleanup complete. Records older than ', @RetentionDays, ' days removed.');
END

GO

-- [dbo].[usp_LoadVeeamBackupJobs]

CREATE PROCEDURE dbo.usp_LoadVeeamBackupJobs  
AS  
BEGIN  
    SET NOCOUNT ON;  
  
    -- Load raw JSON into staging  
    TRUNCATE TABLE dbo.VeeamBackups_Staging;  
  
    INSERT INTO dbo.VeeamBackups_Staging (JobName, JobType, BackupType, Progress, Status, StartTimeRaw, EndTimeRaw, Result)  
    SELECT  
        JobName,  
        JobType,  
        BackupType,  
        Progress,  
        Status,  
        REPLACE(REPLACE(REPLACE(StartTime, '/Date(', ''), ')/', ''), '\', '') AS StartTimeRaw,  
        REPLACE(REPLACE(REPLACE(EndTime, '/Date(', ''), ')/', ''), '\', '') AS EndTimeRaw,  
        Result  
    FROM (  
        SELECT  
            JobName,  
            JobType,  
            BackupType,  
            Progress,  
            Status,  
            StartTime,  
            EndTime,  
            Result  
        FROM OPENROWSET(  
            BULK 'C:\VeeamReports\HourlyBackupJobReport.json',  
            SINGLE_CLOB  
        ) AS j  
        CROSS APPLY OPENJSON(j.BulkColumn)  
        WITH  
        (  
            JobName      NVARCHAR(255) '$.JobName',  
            JobType      NVARCHAR(100) '$.JobType',  
            BackupType   NVARCHAR(100) '$.BackupType',  
            Progress     INT           '$.Progress',  
            Status       NVARCHAR(100) '$.Status',  
            StartTime    NVARCHAR(100) '$.StartTime',  
            EndTime      NVARCHAR(100) '$.EndTime',  
            Result       NVARCHAR(100) '$.Result'  
        )  
    ) AS src;  
  
    -- Convert epoch â†’ datetime2 and insert into final table  
    INSERT INTO ctl.VeeamBackups (JobName, JobType, BackupType, Progress, Status, StartTime, EndTime, Result)  
    SELECT  
        JobName,  
        JobType,  
        BackupType,  
        Progress,  
        Status,  
        DATEADD(MILLISECOND, TRY_CAST(StartTimeRaw AS BIGINT) % 86400000, 
                DATEADD(DAY, TRY_CAST(StartTimeRaw AS BIGINT) / 86400000, '1970-01-01')),  
        DATEADD(MILLISECOND, TRY_CAST(EndTimeRaw AS BIGINT) % 86400000,
                DATEADD(DAY, TRY_CAST(EndTimeRaw AS BIGINT) / 86400000, '1970-01-01')),  
        Result  
    FROM dbo.VeeamBackups_Staging;  
  
END

GO

-- [dbo].[usp_Monitor_JobHealth]

CREATE PROCEDURE dbo.usp_Monitor_JobHealth
    @SendEmail BIT = 0,
    @EmailRecipients NVARCHAR(500) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @FailedCount INT;
    
    SELECT @FailedCount = COUNT(*)
    FROM dbo.vw_FailedJobs_Last24Hours;
    
    IF @FailedCount > 0
    BEGIN
        SELECT 
            JobName,
            FailureTime,
            LEFT(ErrorMessage, 200) AS ErrorMessage
        FROM dbo.vw_FailedJobs_Last24Hours
        ORDER BY FailureTime DESC;
        
        IF @SendEmail = 1 AND @EmailRecipients IS NOT NULL
        BEGIN
            DECLARE @Subject NVARCHAR(255) = 'DBAOps Alert: ' + CAST(@FailedCount AS VARCHAR) + ' Failed Jobs';
            DECLARE @Body NVARCHAR(MAX) = 'The following jobs have failed in the last 24 hours:' + CHAR(13) + CHAR(10);
            
            SELECT @Body = @Body + JobName + ' - ' + CONVERT(VARCHAR, FailureTime, 120) + CHAR(13) + CHAR(10)
            FROM dbo.vw_FailedJobs_Last24Hours;
            
            EXEC msdb.dbo.sp_send_dbmail
                @recipients = @EmailRecipients,
                @subject = @Subject,
                @body = @Body;
        END
    END
    ELSE
        PRINT 'No failed jobs in the last 24 hours.';
END

GO

-- [dbo].[usp_RebuildCalendar]

CREATE PROCEDURE dbo.usp_RebuildCalendar
    @StartDate DATE = '2020-01-01',
    @EndDate DATE = '2030-12-31'
AS
BEGIN
    SET NOCOUNT ON;
    
    TRUNCATE TABLE dim.Calendar;
    
    ;WITH DateRange AS (
        SELECT @StartDate AS dt
        UNION ALL
        SELECT DATEADD(DAY, 1, dt) FROM DateRange WHERE dt < @EndDate
    )
    INSERT INTO dim.Calendar (
        DateKey,
        CalendarDate,
        CalendarYear,
        CalendarQuarter,
        CalendarMonth,
        CalendarDay,
        DayOfWeek,
        DayOfYear,
        WeekOfYear,
        MonthName,
        MonthNameShort,
        DayName,
        DayNameShort,
        FiscalYear,
        FiscalQuarter,
        FiscalMonth,
        IsWeekend,
        IsHoliday,
        IsBusinessDay
    )
    SELECT 
        CONVERT(INT, CONVERT(VARCHAR(8), dt, 112)) AS DateKey,
        dt AS CalendarDate,
        YEAR(dt) AS CalendarYear,
        DATEPART(QUARTER, dt) AS CalendarQuarter,
        MONTH(dt) AS CalendarMonth,
        DAY(dt) AS CalendarDay,
        DATEPART(WEEKDAY, dt) AS DayOfWeek,
        DATEPART(DAYOFYEAR, dt) AS DayOfYear,
        DATEPART(WEEK, dt) AS WeekOfYear,
        DATENAME(MONTH, dt) AS MonthName,
        LEFT(DATENAME(MONTH, dt), 3) AS MonthNameShort,
        DATENAME(WEEKDAY, dt) AS DayName,
        LEFT(DATENAME(WEEKDAY, dt), 3) AS DayNameShort,
        -- Fiscal year (assuming July start)
        CASE WHEN MONTH(dt) >= 7 THEN YEAR(dt) + 1 ELSE YEAR(dt) END AS FiscalYear,
        CASE 
            WHEN MONTH(dt) IN (7,8,9) THEN 1
            WHEN MONTH(dt) IN (10,11,12) THEN 2
            WHEN MONTH(dt) IN (1,2,3) THEN 3
            ELSE 4
        END AS FiscalQuarter,
        CASE 
            WHEN MONTH(dt) >= 7 THEN MONTH(dt) - 6
            ELSE MONTH(dt) + 6
        END AS FiscalMonth,
        CASE WHEN DATEPART(WEEKDAY, dt) IN (1, 7) THEN 1 ELSE 0 END AS IsWeekend,
        0 AS IsHoliday,
        CASE WHEN DATEPART(WEEKDAY, dt) NOT IN (1, 7) THEN 1 ELSE 0 END AS IsBusinessDay
    FROM DateRange
    OPTION (MAXRECURSION 5000);
    
    PRINT 'Calendar rebuilt with ' + CAST(@@ROWCOUNT AS VARCHAR) + ' rows';
END

GO

-- [dbo].[usp_Replication_SendAlerts]

CREATE PROCEDURE dbo.usp_Replication_SendAlerts
    @LatencyThresholdSeconds INT = 600,
    @PendingCommandsThreshold INT = 600,  -- CORRECTED from 10000 to 600
    @EmailRecipients NVARCHAR(500) = 'yourteam@company.com'
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @AlertCount INT = 0;
    DECLARE @Subject NVARCHAR(255);
    DECLARE @Body NVARCHAR(MAX) = '';
    
    -- Check for threshold violations
    IF OBJECT_ID('fact.ReplicationHealth', 'U') IS NOT NULL
    BEGIN
        SELECT @AlertCount = COUNT(*)
        FROM fact.ReplicationHealth
        WHERE CollectionTime >= DATEADD(MINUTE, -30, GETDATE())
        AND (LatencySeconds > @LatencyThresholdSeconds OR PendingCommands > @PendingCommandsThreshold);
        
        IF @AlertCount > 0
        BEGIN
            SET @Subject = 'DBAOps ALERT: Replication Threshold Exceeded (' + CAST(@AlertCount AS VARCHAR) + ' issues)';
            
            SET @Body = 'The following replication issues were detected:' + CHAR(13) + CHAR(10) + CHAR(13) + CHAR(10);
            SET @Body = @Body + 'Thresholds: Latency > ' + CAST(@LatencyThresholdSeconds AS VARCHAR) + 's, Pending Commands > ' + CAST(@PendingCommandsThreshold AS VARCHAR) + CHAR(13) + CHAR(10) + CHAR(13) + CHAR(10);
            
            SELECT @Body = @Body + 
                'Publisher: ' + PublisherName + 
                ' | Publication: ' + PublicationName + 
                ' | Latency: ' + CAST(LatencySeconds AS VARCHAR) + 's' +
                ' | Pending: ' + CAST(PendingCommands AS VARCHAR) + 
                CHAR(13) + CHAR(10)
            FROM fact.ReplicationHealth
            WHERE CollectionTime >= DATEADD(MINUTE, -30, GETDATE())
            AND (LatencySeconds > @LatencyThresholdSeconds OR PendingCommands > @PendingCommandsThreshold);
            
            -- Send email
            EXEC msdb.dbo.sp_send_dbmail
                @recipients = @EmailRecipients,
                @subject = @Subject,
                @body = @Body;
                
            PRINT 'Alert sent for ' + CAST(@AlertCount AS VARCHAR) + ' replication issues';
        END
        ELSE
            PRINT 'No replication threshold violations detected';
    END
END

GO

-- [dbo].[usp_Run_DBAOpsJob]

CREATE   PROCEDURE dbo.usp_Run_DBAOpsJob
(
    @InternalProcedureName NVARCHAR(255)
)
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE 
        @JobName      NVARCHAR(255),
        @StartTime    DATETIME = GETDATE(),
        @EndTime      DATETIME,
        @FailedStep   NVARCHAR(255),
        @ErrorMessage NVARCHAR(MAX),
        @SQL          NVARCHAR(MAX);

    -----------------------------------------------------------------------------------
    -- Detect SQL Agent Job Name
    -----------------------------------------------------------------------------------
    SELECT @JobName = 
        COALESCE(CONVERT(NVARCHAR(255), (SELECT @@PROCID)), 'UNKNOWN JOB');

    IF @JobName LIKE '%_step%'
        SET @JobName = PARSENAME(@JobName, 4);

    -----------------------------------------------------------------------------------
    -- Build dynamic execution for internal DBAOps procedure
    -----------------------------------------------------------------------------------
    SET @SQL = N'EXEC ' + QUOTENAME(@InternalProcedureName) + N';';

    -----------------------------------------------------------------------------------
    -- Execute & Handle Success/Failure
    -----------------------------------------------------------------------------------
    BEGIN TRY
        EXEC (@SQL);

        -----------------------------------------------------------------------------------
        -- SUCCESS Handling
        -----------------------------------------------------------------------------------
        SET @EndTime = GETDATE();

        INSERT INTO dbo.DBAOps_JobExecutionLog
        (
            JobName, StartTime, EndTime, DurationSec, Status
        )
        VALUES
        (
            @JobName,
            @StartTime,
            @EndTime,
            DATEDIFF(SECOND, @StartTime, @EndTime),
            'SUCCESS'
        );

        EXEC dbo.usp_Send_JobSuccess_Alert
            @JobName    = @JobName,
            @StartTime  = @StartTime,
            @EndTime    = @EndTime;

    END TRY
    BEGIN CATCH
        
        -----------------------------------------------------------------------------------
        -- FAILURE Handling
        -----------------------------------------------------------------------------------
        SET @EndTime     = GETDATE();
        SET @FailedStep  = ERROR_PROCEDURE();
        SET @ErrorMessage = ERROR_MESSAGE();

        INSERT INTO dbo.DBAOps_JobExecutionLog
        (
            JobName, StartTime, EndTime, DurationSec, Status, FailedStep, ErrorMessage
        )
        VALUES
        (
            @JobName,
            @StartTime,
            @EndTime,
            DATEDIFF(SECOND, @StartTime, @EndTime),
            'FAILED',
            @FailedStep,
            @ErrorMessage
        );

        EXEC dbo.usp_Send_JobFailure_Alert
            @JobName      = @JobName,
            @StartTime    = @StartTime,
            @EndTime      = @EndTime,
            @FailedStep   = @FailedStep,
            @ErrorMessage = @ErrorMessage;

        THROW;
    END CATCH;
END;

GO

-- [dbo].[usp_Send_DBAOps_DailyHealthReport]

CREATE   PROCEDURE dbo.usp_Send_DBAOps_DailyHealthReport
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @html NVARCHAR(MAX);
    DECLARE @IssueCount INT;
    DECLARE @CriticalCount INT;
    DECLARE @WarningCount INT;
    DECLARE @UnknownCount INT;
    DECLARE @TotalServers INT;
    DECLARE @HealthyCount INT;
    
    -- Get counts using CMS as source of truth
    SELECT 
        @TotalServers = COUNT(DISTINCT cms.server_name),
        @CriticalCount = SUM(CASE WHEN ISNULL(h.HasCriticalAlert, 0) = 1 OR ISNULL(h.HasCriticalDiagnostic, 0) = 1 THEN 1 ELSE 0 END),
        @WarningCount = SUM(CASE WHEN ISNULL(h.OverallHealth, 'UNKNOWN') NOT IN ('HEALTHY', 'UNKNOWN') AND ISNULL(h.HasCriticalAlert, 0) = 0 THEN 1 ELSE 0 END),
        @UnknownCount = SUM(CASE WHEN h.ServerName IS NULL OR ISNULL(h.OverallHealth, 'UNKNOWN') = 'UNKNOWN' THEN 1 ELSE 0 END),
        @HealthyCount = SUM(CASE WHEN ISNULL(h.OverallHealth, 'UNKNOWN') = 'HEALTHY' AND ISNULL(h.HasCriticalAlert, 0) = 0 THEN 1 ELSE 0 END)
    FROM msdb.dbo.sysmanagement_shared_registered_servers_internal cms
    LEFT JOIN dashboard.vw_ServerHealthSummary h 
        ON UPPER(cms.server_name) = UPPER(h.ServerName);
    
    SET @IssueCount = @CriticalCount + @WarningCount + @UnknownCount;
    
    -- Only send if there are issues
    IF @IssueCount = 0
    BEGIN
        PRINT 'DBAOps | JB: All ' + CAST(@TotalServers AS VARCHAR(10)) + ' servers are healthy. No alert sent.';
        RETURN;
    END
    
    -- Build HTML header
    SET @html = N'<html><head><style>
        body { font-family: Segoe UI, Arial, sans-serif; font-size: 12px; }
        h2 { color: #2E86AB; border-bottom: 2px solid #2E86AB; padding-bottom: 5px; }
        h3 { color: #A23B72; }
        table { border-collapse: collapse; margin: 10px 0; }
        th { background-color: #1F497D; color: white; padding: 8px 12px; text-align: left; }
        td { padding: 6px 12px; border: 1px solid #ddd; }
        .critical { background-color: #FADBD8; }
        .warning { background-color: #FCF3CF; }
        .unknown { background-color: #E5E7E9; }
        .healthy { background-color: #D5F5E3; }
        .summary-table td { padding: 4px 15px; }
        .count-good { color: green; font-weight: bold; }
        .count-warning { color: orange; font-weight: bold; }
        .count-critical { color: red; font-weight: bold; }
    </style></head><body>';
    
    SET @html = @html + N'<h2>DBAOps | JB | Daily SQL Server Health Report</h2>';
    SET @html = @html + N'<p><b>Generated:</b> ' + CONVERT(NVARCHAR(30), GETDATE(), 120) + N'<br/>';
    SET @html = @html + N'<b>Author:</b> RHOYLLE INNOVATION EMPIRE™, Sr SQL DBA - DBAOps Engineer<br/>';
    SET @html = @html + N'<b>Source:</b> CMS (Central Management Server)</p>';
    
    -- Summary Section
    SET @html = @html + N'<h3>Health Summary</h3>';
    SET @html = @html + N'<table class="summary-table">';
    SET @html = @html + N'<tr><td><b>Total Servers (CMS):</b></td><td>' + CAST(@TotalServers AS NVARCHAR(10)) + N'</td></tr>';
    SET @html = @html + N'<tr><td><b>Healthy:</b></td><td class="count-good">' + CAST(@HealthyCount AS NVARCHAR(10)) + N'</td></tr>';
    SET @html = @html + N'<tr><td><b>Critical:</b></td><td class="count-critical">' + CAST(@CriticalCount AS NVARCHAR(10)) + N'</td></tr>';
    SET @html = @html + N'<tr><td><b>Warning:</b></td><td class="count-warning">' + CAST(@WarningCount AS NVARCHAR(10)) + N'</td></tr>';
    SET @html = @html + N'<tr><td><b>Unknown/Missing:</b></td><td class="count-warning">' + CAST(@UnknownCount AS NVARCHAR(10)) + N'</td></tr>';
    SET @html = @html + N'</table>';
    
    -- Critical Issues Section
    IF @CriticalCount > 0
    BEGIN
        SET @html = @html + N'<h3 style="color:red;">Critical Issues (' + CAST(@CriticalCount AS NVARCHAR(10)) + N')</h3>';
        SET @html = @html + N'<table><tr><th>Server</th><th>Environment</th><th>SQL Version</th><th>Alerts</th><th>Diagnostics</th><th>Status</th></tr>';
        
        SELECT @html = @html +
            N'<tr class="critical"><td>' + ISNULL(h.ServerName, cms.server_name) + 
            N'</td><td>' + ISNULL(h.Environment, 'N/A') + 
            N'</td><td>' + ISNULL(h.SQLVersion, 'N/A') + 
            N'</td><td>' + CAST(ISNULL(h.ActiveAlertCount, 0) AS NVARCHAR(10)) + 
            N'</td><td>' + CAST(ISNULL(h.OpenDiagnosticCount, 0) AS NVARCHAR(10)) + 
            N'</td><td>' + ISNULL(h.OverallHealth, 'UNKNOWN') + 
            N'</td></tr>'
        FROM msdb.dbo.sysmanagement_shared_registered_servers_internal cms
        LEFT JOIN dashboard.vw_ServerHealthSummary h 
            ON UPPER(cms.server_name) = UPPER(h.ServerName)
        WHERE ISNULL(h.HasCriticalAlert, 0) = 1 OR ISNULL(h.HasCriticalDiagnostic, 0) = 1
        ORDER BY h.ServerName;
        
        SET @html = @html + N'</table>';
    END
    
    -- Warning Section
    IF @WarningCount > 0
    BEGIN
        SET @html = @html + N'<h3 style="color:orange;">Warnings (' + CAST(@WarningCount AS NVARCHAR(10)) + N')</h3>';
        SET @html = @html + N'<table><tr><th>Server</th><th>Environment</th><th>SQL Version</th><th>Alerts</th><th>Status</th></tr>';
        
        SELECT @html = @html +
            N'<tr class="warning"><td>' + ISNULL(h.ServerName, cms.server_name) + 
            N'</td><td>' + ISNULL(h.Environment, 'N/A') + 
            N'</td><td>' + ISNULL(h.SQLVersion, 'N/A') + 
            N'</td><td>' + CAST(ISNULL(h.ActiveAlertCount, 0) AS NVARCHAR(10)) + 
            N'</td><td>' + ISNULL(h.OverallHealth, 'UNKNOWN') + 
            N'</td></tr>'
        FROM msdb.dbo.sysmanagement_shared_registered_servers_internal cms
        LEFT JOIN dashboard.vw_ServerHealthSummary h 
            ON UPPER(cms.server_name) = UPPER(h.ServerName)
        WHERE ISNULL(h.OverallHealth, 'UNKNOWN') NOT IN ('HEALTHY', 'UNKNOWN') 
        AND ISNULL(h.HasCriticalAlert, 0) = 0
        ORDER BY h.ServerName;
        
        SET @html = @html + N'</table>';
    END
    
    -- Unknown/Missing Section
    IF @UnknownCount > 0
    BEGIN
        SET @html = @html + N'<h3 style="color:gray;">Unknown/Missing Data (' + CAST(@UnknownCount AS NVARCHAR(10)) + N')</h3>';
        SET @html = @html + N'<table><tr><th>Server (from CMS)</th><th>Status</th></tr>';
        
        SELECT @html = @html +
            N'<tr class="unknown"><td>' + cms.server_name + 
            N'</td><td>' + ISNULL(h.OverallHealth, 'NO DATA') + 
            N'</td></tr>'
        FROM msdb.dbo.sysmanagement_shared_registered_servers_internal cms
        LEFT JOIN dashboard.vw_ServerHealthSummary h 
            ON UPPER(cms.server_name) = UPPER(h.ServerName)
        WHERE h.ServerName IS NULL OR ISNULL(h.OverallHealth, 'UNKNOWN') = 'UNKNOWN'
        ORDER BY cms.server_name;
        
        SET @html = @html + N'</table>';
    END
    
    -- Footer
    SET @html = @html + N'<hr/><p><i>This report only includes servers with issues. Healthy servers are excluded.</i></p>';
    SET @html = @html + N'</body></html>';
    
    -- Get mail profile
    DECLARE @ProfileName SYSNAME;
    SELECT TOP 1 @ProfileName = name FROM msdb.dbo.sysmail_profile ORDER BY name;
    
    DECLARE @Subject NVARCHAR(255) = N'DBAOps | JB | Daily Health Report - ' + 
        CAST(@CriticalCount AS NVARCHAR(10)) + N' Critical, ' +
        CAST(@WarningCount AS NVARCHAR(10)) + N' Warnings';
    
    -- Send email
    EXEC msdb.dbo.sp_send_dbmail
        @profile_name = @ProfileName,
        @recipients = 'SQLServerAdminNotification@YOURORG.gov',
        @subject = @Subject,
        @body = @html,
        @body_format = 'HTML';
    
    PRINT 'DBAOps | JB: Daily Health Report sent. Critical: ' + CAST(@CriticalCount AS VARCHAR(10)) + 
          ', Warnings: ' + CAST(@WarningCount AS VARCHAR(10)) + 
          ', Unknown: ' + CAST(@UnknownCount AS VARCHAR(10));
END;

GO

-- [dbo].[usp_Send_EOS_WeeklyReport]

CREATE PROCEDURE [dbo].[usp_Send_EOS_WeeklyReport]  
    @Recipients VARCHAR(MAX) = 'jboscoRHOYLLE INNOVATION EMPIRE™@YOURORG.gov',  
    @CCRecipients VARCHAR(MAX) = NULL,  
    @SendIfNoIssues BIT = 0
AS  
BEGIN  
    SET NOCOUNT ON;  
  
    DECLARE @Subject NVARCHAR(255)  
    DECLARE @Body NVARCHAR(MAX)  
    DECLARE @TableHTML NVARCHAR(MAX)  
    DECLARE @SummaryHTML NVARCHAR(MAX)  
    DECLARE @CriticalCount INT  
    DECLARE @EOLCount INT  
    DECLARE @WarningCount INT  
    DECLARE @TotalServers INT  
  
    -- Get counts  
    SELECT @TotalServers = COUNT(*) FROM dbo.vw_SQLServer_EOS_Status;  
    SELECT @EOLCount = COUNT(*) FROM dbo.vw_SQLServer_EOS_Status WHERE UrgencyLevel = 'END OF LIFE';  
    SELECT @CriticalCount = COUNT(*) FROM dbo.vw_SQLServer_EOS_Status WHERE UrgencyLevel = 'CRITICAL (<90 days)';  
    SELECT @WarningCount = COUNT(*) FROM dbo.vw_SQLServer_EOS_Status WHERE UrgencyLevel IN ('HIGH (<180 days)', 'MEDIUM (<1 year)');  
  
    -- Exit if no issues and @SendIfNoIssues = 0  
    IF @EOLCount = 0 AND @CriticalCount = 0 AND @WarningCount = 0 AND @SendIfNoIssues = 0  
    BEGIN  
        PRINT 'No EOS issues found. Email not sent.'  
        RETURN;  
    END  
  
    -- Set subject based on urgency  
    SET @Subject = CASE   
        WHEN @EOLCount > 0 THEN 'ðŸ”´ CRITICAL: ' + CAST(@EOLCount AS VARCHAR) + ' SQL Servers END OF LIFE - Weekly EOS Report'  
        WHEN @CriticalCount > 0 THEN 'ðŸŸ  WARNING: ' + CAST(@CriticalCount AS VARCHAR) + ' SQL Servers approaching EOS (<90 days)'  
        WHEN @WarningCount > 0 THEN 'ðŸŸ¡ NOTICE: ' + CAST(@WarningCount AS VARCHAR) + ' SQL Servers require EOS planning'  
        ELSE 'âœ… SQL Server EOS Weekly Report - All Systems Supported'  
    END  
  
    -- Build summary section  
    SET @SummaryHTML = N'  
    <h2 style="color: #1F4E79;">Executive Summary</h2>  
    <table style="border-collapse: collapse; width: 50%; margin-bottom: 20px;">  
        <tr style="background-color: #1F4E79; color: white;">  
            <th style="padding: 10px; border: 1px solid #ddd;">Status</th>  
            <th style="padding: 10px; border: 1px solid #ddd;">Count</th>  
        </tr>  
        <tr style="background-color: #FFCCCC;">  
            <td style="padding: 10px; border: 1px solid #ddd;">ðŸ”´ End of Life</td>  
            <td style="padding: 10px; border: 1px solid #ddd; text-align: center; font-weight: bold;">' + CAST(@EOLCount AS VARCHAR) + '</td>  
        </tr>  
        <tr style="background-color: #FFE6CC;">  
            <td style="padding: 10px; border: 1px solid #ddd;">ðŸŸ  Critical (&lt;90 days)</td>  
            <td style="padding: 10px; border: 1px solid #ddd; text-align: center; font-weight: bold;">' + CAST(@CriticalCount AS VARCHAR) + '</td>  
        </tr>  
        <tr style="background-color: #FFFFCC;">  
            <td style="padding: 10px; border: 1px solid #ddd;">ðŸŸ¡ Warning (&lt;1 year)</td>  
            <td style="padding: 10px; border: 1px solid #ddd; text-align: center; font-weight: bold;">' + CAST(@WarningCount AS VARCHAR) + '</td>  
        </tr>  
        <tr style="background-color: #E2EFDA;">  
            <td style="padding: 10px; border: 1px solid #ddd;">ðŸŸ¢ Supported</td>  
            <td style="padding: 10px; border: 1px solid #ddd; text-align: center; font-weight: bold;">' + CAST(@TotalServers - @EOLCount - @CriticalCount - @WarningCount AS VARCHAR) + '</td>  
        </tr>  
        <tr style="background-color: #F2F2F2; font-weight: bold;">  
            <td style="padding: 10px; border: 1px solid #ddd;">Total Servers</td>  
            <td style="padding: 10px; border: 1px solid #ddd; text-align: center;">' + CAST(@TotalServers AS VARCHAR) + '</td>  
        </tr>  
    </table>'  
  
    -- Build version summary table  
    SET @SummaryHTML = @SummaryHTML + N'  
    <h2 style="color: #1F4E79;">Summary by SQL Server Version</h2>  
    <table style="border-collapse: collapse; width: 80%; margin-bottom: 20px;">  
        <tr style="background-color: #1F4E79; color: white;">  
         <th style="padding: 10px; border: 1px solid #ddd;">SQL Version</th>  
            <th style="padding: 10px; border: 1px solid #ddd;">Server Count</th>  
            <th style="padding: 10px; border: 1px solid #ddd;">End of Support</th>  
            <th style="padding: 10px; border: 1px solid #ddd;">Days Remaining</th>  
            <th style="padding: 10px; border: 1px solid #ddd;">Status</th>  
        </tr>'  
  
    SELECT @SummaryHTML = @SummaryHTML + N'  
        <tr style="background-color: ' +   
            CASE   
                WHEN Status = 'END OF LIFE' THEN '#FFCCCC'  
                WHEN Status = 'ACTION REQUIRED' THEN '#FFE6CC'  
                ELSE '#E2EFDA'  
            END + ';">  
            <td style="padding: 8px; border: 1px solid #ddd;">' + ISNULL(SQLVersionName, 'Unknown') + '</td>  
            <td style="padding: 8px; border: 1px solid #ddd; text-align: center;">' + CAST(ServerCount AS VARCHAR) + '</td>  
            <td style="padding: 8px; border: 1px solid #ddd; text-align: center;">' + ISNULL(CONVERT(VARCHAR, EndOfSupport, 107), 'N/A') + '</td>  
            <td style="padding: 8px; border: 1px solid #ddd; text-align: center;">' + CAST(ISNULL(DaysUntilEOS, 0) AS VARCHAR) + '</td>  
            <td style="padding: 8px; border: 1px solid #ddd;">' + ISNULL(Status, 'Unknown') + '</td>  
        </tr>'  
    FROM dbo.vw_SQLServer_EOS_Summary  
    ORDER BY EndOfSupport;  
  
    SET @SummaryHTML = @SummaryHTML + N'</table>'  
  
    -- Build detailed server table (only for EOL and Critical)  
    IF @EOLCount > 0 OR @CriticalCount > 0  
    BEGIN  
        SET @TableHTML = N'  
        <h2 style="color: #C00000;">âš ï¸ Servers Requiring Immediate Attention</h2>  
        <table style="border-collapse: collapse; width: 100%; margin-bottom: 20px;">  
            <tr style="background-color: #C00000; color: white;">  
                <th style="padding: 10px; border: 1px solid #ddd;">Server Name</th>  
                <th style="padding: 10px; border: 1px solid #ddd;">Instance</th>  
                <th style="padding: 10px; border: 1px solid #ddd;">SQL Version</th>  
                <th style="padding: 10px; border: 1px solid #ddd;">Edition</th>  
                <th style="padding: 10px; border: 1px solid #ddd;">End of Support</th>  
                <th style="padding: 10px; border: 1px solid #ddd;">Days Left</th>  
                <th style="padding: 10px; border: 1px solid #ddd;">Urgency</th>  
                <th style="padding: 10px; border: 1px solid #ddd;">Recommended Action</th>  
            </tr>'  
  
        SELECT @TableHTML = @TableHTML + N'  
            <tr style="background-color: ' +   
                CASE   
                    WHEN UrgencyLevel = 'END OF LIFE' THEN '#FFCCCC'  
                    ELSE '#FFE6CC'  
                END + ';">  
                <td style="padding: 8px; border: 1px solid #ddd;">' + ServerName + '</td>  
                <td style="padding: 8px; border: 1px solid #ddd;">' + ISNULL(InstanceName, 'Default') + '</td>  
                <td style="padding: 8px; border: 1px solid #ddd;">' + ISNULL(SQLVersion, '') + '</td>  
                <td style="padding: 8px; border: 1px solid #ddd;">' + ISNULL(Edition, '') + '</td>  
                <td style="padding: 8px; border: 1px solid #ddd; text-align: center;">' + ISNULL(CONVERT(VARCHAR, EndOfSupport, 107), 'N/A') + '</td>  
                <td style="padding: 8px; border: 1px solid #ddd; text-align: center; font-weight: bold;">' + CAST(ISNULL(DaysUntilEOS, 0) AS VARCHAR) + '</td>  
                <td style="padding: 8px; border: 1px solid #ddd;">' + UrgencyLevel + '</td>  
                <td style="padding: 8px; border: 1px solid #ddd;">' + ISNULL(RecommendedAction, '') + '</td>  
            </tr>'  
        FROM dbo.vw_SQLServer_EOS_Status  
        WHERE UrgencyLevel IN ('END OF LIFE', 'CRITICAL (<90 days)')  
        ORDER BY DaysUntilEOS, ServerName;  
  
        SET @TableHTML = @TableHTML + N'</table>'  
    END  
    ELSE  
    BEGIN  
        SET @TableHTML = N''  
    END  
  
    -- Build complete email body  
    SET @Body = N'  
    <html>  
    <head>  
        <style>  
            body { font-family: Arial, sans-serif; }  
            h1 { color: #1F4E79; }  
            h2 { color: #2E75B6; margin-top: 30px; }  
        </style>  
    </head>  
    <body>  
        <h1>ðŸ–¥ï¸ SQL Server End of Service (EOS) Weekly Report</h1>  
        <p><strong>Report Date:</strong> ' + CONVERT(VARCHAR, GETDATE(), 107) + '</p>  
        <p><strong>Generated By:</strong> DBAOps Monitoring Framework</p>  
        <hr style="border: 1px solid #1F4E79;">  
        ' + @SummaryHTML + '  
        ' + @TableHTML + '  
        <hr style="border: 1px solid #1F4E79;">  
        <h2 style="color: #1F4E79;">Microsoft SQL Server Lifecycle Reference</h2>  
        <table style="border-collapse: collapse; width: 60%;">  
            <tr style="background-color: #1F4E79; color: white;">  
                <th style="padding: 8px; border: 1px solid #ddd;">Version</th>  
                <th style="padding: 8px; border: 1px solid #ddd;">Extended Support End</th>  
                <th style="padding: 8px; border: 1px solid #ddd;">Status</th>  
            </tr>  
            <tr><td style="padding: 6px; border: 1px solid #ddd;">SQL Server 2014</td><td style="padding: 6px; border: 1px solid #ddd;">July 9, 2024</td><td style="padding: 6px; border: 1px solid #ddd; color: red;">End of Life</td></tr>  
            <tr><td style="padding: 6px; border: 1px solid #ddd;">SQL Server 2016</td><td style="padding: 6px; border: 1px solid #ddd;">July 14, 2026</td><td style="padding: 6px; border: 1px solid #ddd; color: orange;">Extended Support</td></tr>  
            <tr><td style="padding: 6px; border: 1px solid #ddd;">SQL Server 2017</td><td style="padding: 6px; border: 1px solid #ddd;">October 12, 2027</td><td style="padding: 6px; border: 1px solid #ddd; color: green;">Extended Support</td></tr>  
            <tr><td style="padding: 6px; border: 1px solid #ddd;">SQL Server 2019</td><td style="padding: 6px; border: 1px solid #ddd;">January 8, 2030</td><td style="padding: 6px; border: 1px solid #ddd; color: green;">Mainstream</td></tr>  
            <tr><td style="padding: 6px; border: 1px solid #ddd;">SQL Server 2022</td><td style="padding: 6px; border: 1px solid #ddd;">January 11, 2033</td><td style="padding: 6px; border: 1px solid #ddd; color: green;">Mainstream</td></tr>  
            <tr><td style="padding: 6px; border: 1px solid #ddd;">SQL Server 2025</td><td style="padding: 6px; border: 1px solid #ddd;">November 1, 2035</td><td style="padding: 6px; border: 1px solid #ddd; color: green;">Full Support</td></tr>  
        </table>  
        <br>  
        <p style="color: #666; font-size: 11px;">  
            This report is generated automatically by the DBAOps Monitoring Framework.<br>  
            For questions, contact: Eng. John Bosco RHOYLLE INNOVATION EMPIRE™, SQL DBA/DBAOps Architect<br>  
            Dashboard: <a href="http://yourserver/dbaops/eos">DBAOps EOS Dashboard</a>  
        </p>  
    </body>  
    </html>'  
  
    -- =====================================================
    -- FIXED: Changed from 'DBAOps' to 'DBAOps_SQLPatching'
    -- =====================================================
    EXEC msdb.dbo.sp_send_dbmail  
        @profile_name = 'DBAOps_SQLPatching',
        @recipients = @Recipients,  
        @copy_recipients = @CCRecipients,  
        @subject = @Subject,  
        @body = @Body,  
        @body_format = 'HTML';  
  
    PRINT 'EOS Weekly Report sent successfully to: ' + @Recipients;  
END

GO

-- [dbo].[usp_Send_JobFailure_Alert]

CREATE   PROCEDURE dbo.usp_Send_JobFailure_Alert
(
    @JobName        NVARCHAR(255),
    @StartTime      DATETIME,
    @EndTime        DATETIME,
    @FailedStep     NVARCHAR(255),
    @ErrorMessage   NVARCHAR(MAX)
)
AS
BEGIN
    SET NOCOUNT ON;

    ----------------------------------------------------------------------------------------------------
    -- Declare variables
    ----------------------------------------------------------------------------------------------------
    DECLARE @Body    NVARCHAR(MAX);
    DECLARE @Subject NVARCHAR(255);

    ----------------------------------------------------------------------------------------------------
    -- Build HTML Body
    ----------------------------------------------------------------------------------------------------
    SET @Body =
        N'<h2 style="color:red;">FAILURE: ' + @JobName + N'</h2>' +
        N'<p>The DBAOps job has failed.</p><br>' +

        N'<b>StartTime:</b> ' + CONVERT(NVARCHAR(30), @StartTime, 120) + N'<br>' +
        N'<b>EndTime:</b> '   + CONVERT(NVARCHAR(30), @EndTime,   120) + N'<br>' +
        N'<b>Duration:</b> '  + CONVERT(NVARCHAR(30), DATEDIFF(SECOND, @StartTime, @EndTime)) + N' seconds<br><br>' +

        N'<b>Failed Step:</b> ' + ISNULL(@FailedStep, N'UNKNOWN') + N'<br><br>' +
        N'<b>Error Message:</b><br>' +
        N'<div style="color:red;">' +
            REPLACE(ISNULL(@ErrorMessage, N'No message returned.'), CHAR(10), '<br>') +
        N'</div><br><br>' +

        N'<hr><p style="font-size:12px;">This alert was generated automatically by the DBAOps Notification Engine.</p>';

    ----------------------------------------------------------------------------------------------------
    -- Build Subject Line
    ----------------------------------------------------------------------------------------------------
    SET @Subject = N'*** DBAOps FAILURE: ' + @JobName + N' ***';

    ----------------------------------------------------------------------------------------------------
    -- Send Email
    ----------------------------------------------------------------------------------------------------
    EXEC msdb.dbo.sp_send_dbmail
         @profile_name = 'DBAOpsProfile',
         @recipients   = 'SQLServerAdminNotification@YOURORG.gov',
         @subject      = @Subject,
         @body         = @Body,
         @body_format  = 'HTML';
END;

GO

-- [dbo].[usp_Send_JobSuccess_Alert]

CREATE   PROCEDURE dbo.usp_Send_JobSuccess_Alert
(
    @JobName NVARCHAR(255),
    @StartTime DATETIME,
    @EndTime DATETIME
)
AS
BEGIN
    -- SUCCESS ALERTS DISABLED - Dashboard shows status
    -- This procedure intentionally does nothing
    PRINT 'DBAOps | JB: Success alert suppressed for ' + @JobName + '. Dashboard shows status.';
END;

GO

-- [dbo].[usp_Send_JobSuccess_Alert_DEPRECATED]

CREATE   PROCEDURE dbo.usp_Send_JobSuccess_Alert
(
    @JobName NVARCHAR(255),
    @StartTime DATETIME,
    @EndTime DATETIME
)
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @Body NVARCHAR(MAX);
    DECLARE @Subject NVARCHAR(255);

    SET @Body = 
        N'<h2 style="color:green;">SUCCESS - ' + @JobName + N'</h2>' +
        N'<p>The job completed successfully.</p>' +
        N'<b>StartTime:</b> ' + CONVERT(NVARCHAR(30), @StartTime, 120) + N'<br>' +
        N'<b>EndTime:</b> ' + CONVERT(NVARCHAR(30), @EndTime, 120) + N'<br><br>';

    SET @Subject = N'*** DBAOps SUCCESS: ' + @JobName + N' ***';

    EXEC msdb.dbo.sp_send_dbmail
        @profile_name = 'DBAOpsProfile',
        @recipients = 'SQLServerAdminNotification@YOURORG.gov',
        @subject = @Subject,
        @body = @Body,
        @body_format = 'HTML';
END

GO

-- [dbo].[usp_Send_UnhealthyJobAlerts]

CREATE   PROCEDURE dbo.usp_Send_UnhealthyJobAlerts
AS
BEGIN
    SET NOCOUNT ON;

    SELECT *
    FROM dbo.vJobScheduleCompliance
    WHERE ComplianceStatus <> 'Compliant';
END

GO

-- [dbo].[usp_SendAlertEmails]

CREATE   PROCEDURE dbo.usp_SendAlertEmails
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE 
        @AlertID BIGINT,
        @ServerName NVARCHAR(128),
        @AlertType NVARCHAR(100),
        @AlertMessage NVARCHAR(MAX),
        @SafeMessage NVARCHAR(MAX),
        @Subject NVARCHAR(300);

    DECLARE alert_cursor CURSOR LOCAL STATIC READ_ONLY FOR
    SELECT AlertID, ServerName, AlertType, AlertMessage
    FROM dbo.fact_Alerts
    WHERE Processed = 0;

    OPEN alert_cursor;
    FETCH NEXT FROM alert_cursor INTO @AlertID, @ServerName, @AlertType, @AlertMessage;

    WHILE @@FETCH_STATUS = 0
    BEGIN
        -- Escape single quotes for safety
        SET @SafeMessage = REPLACE(@AlertMessage, '''', '''''');

        -- Build subject safely
        SET @Subject = 'SQL ALERT: ' + @AlertType + ' on ' + @ServerName;

        -- Send Email
        EXEC msdb.dbo.sp_send_dbmail
            @profile_name = 'DBAOpsProfile',
            @recipients   = 'SQLServerAdminNotification@YOURORG.gov',
            @subject      = @Subject,
            @body         = @SafeMessage;

        -- Mark as processed
        UPDATE dbo.fact_Alerts
        SET Processed = 1,
            ProcessedTime = GETDATE()
        WHERE AlertID = @AlertID;

        FETCH NEXT FROM alert_cursor INTO @AlertID, @ServerName, @AlertType, @AlertMessage;
    END

    CLOSE alert_cursor;
    DEALLOCATE alert_cursor;
END

GO

-- [dbo].[usp_SendDailySummaryEmail]

CREATE PROCEDURE dbo.usp_SendDailySummaryEmail
    @OnlyOnIssues BIT = 1  -- NEW: Only send if issues exist
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE 
        @TemplateHTML NVARCHAR(MAX),
        @SubjectLine NVARCHAR(255),
        @AlertSummary NVARCHAR(MAX),
        @PerfSummary NVARCHAR(MAX),
        @AlertList NVARCHAR(MAX),
        @FinalBody NVARCHAR(MAX),
        @AlertCount INT = 0,
        @CriticalCount INT = 0,
        @WarningCount INT = 0;
    
    ----------------------------------------------------
    -- 1. Check if there are any alerts/issues
    ----------------------------------------------------
    
    -- Get alert counts from fact_Alerts (unprocessed)
    IF OBJECT_ID('dbo.fact_Alerts', 'U') IS NOT NULL
    BEGIN
        SELECT 
            @AlertCount = COUNT(*),
            @CriticalCount = SUM(CASE WHEN AlertType LIKE '%CRITICAL%' OR AlertType LIKE '%SEVERE%' THEN 1 ELSE 0 END),
            @WarningCount = SUM(CASE WHEN AlertType LIKE '%WARNING%' THEN 1 ELSE 0 END)
        FROM dbo.fact_Alerts
        WHERE Processed = 0 
           OR ProcessedTime >= DATEADD(DAY, -1, GETDATE());
    END;
    
    -- =====================================================
    -- SMART EMAIL LOGIC: Only send if issues exist
    -- =====================================================
    IF @OnlyOnIssues = 1 AND @AlertCount = 0
    BEGIN
        PRINT 'DBAOps | JB: No alerts in last 24 hours. Daily summary email not sent.';
        PRINT 'Full status available in dashboard.';
        RETURN;
    END;
    
    ----------------------------------------------------
    -- 2. Load template from EmailTemplates table
    ----------------------------------------------------
    SELECT 
        @TemplateHTML = HtmlContent,
        @SubjectLine = SubjectLine
    FROM dbo.EmailTemplates
    WHERE TemplateName = 'DailySummary';
    
    IF @TemplateHTML IS NULL
    BEGIN
        RAISERROR('DailySummary template missing!', 16, 1);
        RETURN;
    END
    
    -- Update subject line with alert counts
    IF @CriticalCount > 0
        SET @SubjectLine = 'ðŸš¨ ' + @SubjectLine + ' - ' + CAST(@CriticalCount AS VARCHAR) + ' Critical';
    ELSE IF @WarningCount > 0
        SET @SubjectLine = 'âš ï¸ ' + @SubjectLine + ' - ' + CAST(@WarningCount AS VARCHAR) + ' Warning';
    ELSE IF @AlertCount > 0
        SET @SubjectLine = 'ðŸ“Š ' + @SubjectLine + ' - ' + CAST(@AlertCount AS VARCHAR) + ' Alerts';
    
    ----------------------------------------------------
    -- 3. Get HTML fragments from builder proc
    ----------------------------------------------------
    DECLARE @HTML TABLE (
        AlertSummaryHtml NVARCHAR(MAX),
        PerformanceSummaryHtml NVARCHAR(MAX),
        AlertListHtml NVARCHAR(MAX)
    );
    
    INSERT INTO @HTML
    EXEC dbo.usp_BuildDailySummaryHtml;
    
    SELECT 
        @AlertSummary = AlertSummaryHtml,
        @PerfSummary = PerformanceSummaryHtml,
        @AlertList = AlertListHtml
    FROM @HTML;
    
    ----------------------------------------------------
    -- 4. Replace placeholders with dynamic HTML
    ----------------------------------------------------
    SET @FinalBody = @TemplateHTML;
    
    SET @FinalBody = REPLACE(@FinalBody, '{{AlertSummary}}', @AlertSummary);
    SET @FinalBody = REPLACE(@FinalBody, '{{PerformanceSummary}}', @PerfSummary);
    SET @FinalBody = REPLACE(@FinalBody, '{{AlertList}}', @AlertList);
    
    -- Add note about smart alerting
    SET @FinalBody = REPLACE(@FinalBody, '</body>', 
        N'<p style="color:gray;font-size:11px;margin-top:20px;">This email is only sent when alerts or issues are detected. Full status available in dashboard.</p></body>');
    
    ----------------------------------------------------
    -- 5. Send Email
    ----------------------------------------------------
    EXEC msdb.dbo.sp_send_dbmail
        @profile_name = 'DBAOpsProfile',
        @recipients = 'SQLServerAdminNotification@YOURORG.gov',
        @subject = @SubjectLine,
        @body = @FinalBody,
        @body_format = 'HTML';
    
    PRINT 'DBAOps | JB: Daily summary sent with ' + CAST(@AlertCount AS VARCHAR) + ' alerts.';
END;

GO

-- [dbo].[usp_SendDBAOpsFailureAlert]

CREATE PROCEDURE dbo.usp_SendDBAOpsFailureAlert
      @JobName SYSNAME
    , @StepName NVARCHAR(200)
    , @Message  NVARCHAR(MAX)
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @SeverityLevel INT;
    DECLARE @SeverityLabel VARCHAR(20);
    DECLARE @EmailSubject NVARCHAR(400);
    DECLARE @EmailBody NVARCHAR(MAX);
    DECLARE @PatchWindow BIT = 0;

    -------------------------------------------------------------
    -- 1. LOOKUP SEVERITY FROM DBAOps_JobSeverity
    -------------------------------------------------------------
    SELECT @SeverityLevel = SeverityLevel
    FROM dbo.DBAOps_JobSeverity
    WHERE JobName = @JobName;

    IF @SeverityLevel IS NULL
        SET @SeverityLevel = 3;  -- Default to MEDIUM if not found

    -------------------------------------------------------------
    -- 2. LABEL SEVERITY
    -------------------------------------------------------------
    SET @SeverityLabel =
        CASE @SeverityLevel
            WHEN 1 THEN 'CRITICAL'
            WHEN 2 THEN 'HIGH'
            WHEN 3 THEN 'MEDIUM'
            WHEN 4 THEN 'LOW'
        END;

    -------------------------------------------------------------
    -- 3. DETECT PATCH WINDOW (Saturday 8â€“10 PM MST)
    -------------------------------------------------------------
    IF(
        DATENAME(WEEKDAY, GETDATE()) = 'Saturday' AND
        DATEPART(HOUR, GETDATE()) BETWEEN 20 AND 22
    )
        SET @PatchWindow = 1;

    -------------------------------------------------------------
    -- 4. SUPPRESSION LOGIC DURING PATCH WINDOW
    -------------------------------------------------------------
    IF @PatchWindow = 1 AND @SeverityLevel >= 3
    BEGIN
        PRINT 'Alert suppressed due to patch window (MEDIUM/LOW severity).';
        RETURN;
    END;

    -------------------------------------------------------------
    -- 5. BUILD EMAIL SUBJECT + BODY
    -------------------------------------------------------------
    SET @EmailSubject = CONCAT(
        '[DBAOps][Severity ', @SeverityLevel, ' - ', @SeverityLabel,
        '] Job Failure: ', @JobName
    );

    SET @EmailBody = '
Job Name: ' + @JobName + '
Step Name: ' + @StepName + '
Severity Level: ' + @SeverityLabel + '
Timestamp: ' + CONVERT(VARCHAR(30), GETDATE(), 120) + '

Message:
------------------------------------------------------------
' + @Message + '
------------------------------------------------------------';

    -------------------------------------------------------------
    -- 6. SEND EMAIL
    -------------------------------------------------------------
    EXEC msdb.dbo.sp_send_dbmail
        @profile_name = 'DBAOpsProfile',
        @recipients   = 'SQLServerAdminNotification@YOURORG.gov',
        @subject      = @EmailSubject,
        @body         = @EmailBody;

END

GO

-- [dbo].[usp_SendServerHealthReport]

CREATE PROCEDURE dbo.usp_SendServerHealthReport
    @OnlyOnIssues BIT = 1  -- NEW: Only send if issues detected
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @html NVARCHAR(MAX);
    DECLARE @IssueCount INT = 0;
    DECLARE @TotalServers INT = 0;
    
    -- Check for issues first
    ;WITH Latest AS (
        SELECT *
        FROM (
            SELECT *,
                   ROW_NUMBER() OVER (PARTITION BY ServerName ORDER BY CaptureTime DESC) AS rn
            FROM dbo.FACT_ServerHealth
        ) X
        WHERE rn = 1
    )
    SELECT 
        @TotalServers = COUNT(*),
        @IssueCount = SUM(CASE 
            WHEN CPU_SignalWaitPct > 20 THEN 1
            WHEN Memory_PLE < 300 THEN 1
            WHEN Memory_GrantsPending > 5 THEN 1
            WHEN BlockingSessions > 0 THEN 1
            WHEN BackupAge_Log_Hours > 24 THEN 1
            ELSE 0 END)
    FROM Latest;
    
    -- =====================================================
    -- SMART EMAIL LOGIC: Only send if issues detected
    -- =====================================================
    IF @OnlyOnIssues = 1 AND @IssueCount = 0
    BEGIN
        PRINT 'DBAOps | JB: All ' + CAST(@TotalServers AS VARCHAR(10)) + 
            ' servers healthy. Email not sent.';
        RETURN;
    END;
    
    ;WITH Latest AS (
        SELECT *
        FROM (
            SELECT *,
                   ROW_NUMBER() OVER (PARTITION BY ServerName ORDER BY CaptureTime DESC) AS rn
            FROM dbo.FACT_ServerHealth
        ) X
        WHERE rn = 1
    )
    SELECT @html =
    N'<html><body>
    <h2 style="font-family:Segoe UI;color:#003366;">DBAOps â€“ SQL Server Health Summary</h2>
    <p style="font-family:Segoe UI;">Generated: ' + CONVERT(NVARCHAR(30), GETDATE(), 120) + '</p>
    <p><strong>This report only sent when issues detected. ' + CAST(@IssueCount AS NVARCHAR) + ' issues found.</strong></p>
    
    <table border="1" cellpadding="6" cellspacing="0" style="border-collapse:collapse;font-family:Segoe UI;font-size:11px;">
    <tr style="background-color:#003366;color:white;font-weight:bold;text-align:center;">
        <th>Server</th>
        <th>Signal Wait %</th>
        <th>PLE</th>
        <th>Grants Pending</th>
        <th>Blocking</th>
        <th>Log Backup Age (hrs)</th>
    </tr>' +
    (
        SELECT 
            '<tr style="text-align:center;' + 
            CASE WHEN CPU_SignalWaitPct > 20 OR Memory_PLE < 300 OR BlockingSessions > 0 
                 THEN 'background-color:#ffe0e0;' ELSE '' END + '">' +
            '<td>' + ServerName + '</td>' +
            '<td style="' + CASE WHEN CPU_SignalWaitPct > 20 THEN 'color:red;font-weight:bold;' ELSE '' END + '">' + ISNULL(CAST(CPU_SignalWaitPct AS NVARCHAR(20)),'') + '</td>' +
            '<td style="' + CASE WHEN Memory_PLE < 300 THEN 'color:red;font-weight:bold;' ELSE '' END + '">' + ISNULL(CAST(Memory_PLE AS NVARCHAR(20)),'') + '</td>' +
            '<td>' + ISNULL(CAST(Memory_GrantsPending AS NVARCHAR(20)),'') + '</td>' +
            '<td style="' + CASE WHEN BlockingSessions > 0 THEN 'color:orange;font-weight:bold;' ELSE '' END + '">' + ISNULL(CAST(BlockingSessions AS NVARCHAR(20)),'') + '</td>' +
            '<td style="' + CASE WHEN BackupAge_Log_Hours > 24 THEN 'color:red;font-weight:bold;' ELSE '' END + '">' + ISNULL(CAST(BackupAge_Log_Hours AS NVARCHAR(20)),'') + '</td>' +
            '</tr>'
        FROM Latest
        WHERE CPU_SignalWaitPct > 20 OR Memory_PLE < 300 OR Memory_GrantsPending > 5 
              OR BlockingSessions > 0 OR BackupAge_Log_Hours > 24
        FOR XML PATH(''), TYPE
    ).value('.', 'NVARCHAR(MAX)')
    + '</table></body></html>';
    
    EXEC msdb.dbo.sp_send_dbmail
        @profile_name = 'DBAOpsProfile',
        @recipients = 'SQLServerAdminNotification@YOURORG.gov',
        @subject = 'DBAOps â€“ SQL Server Health Alert - Issues Detected',
        @body = @html,
        @body_format = 'HTML';
    
    PRINT 'DBAOps | JB: Health report sent - ' + CAST(@IssueCount AS VARCHAR) + ' issues';
END;

GO

-- [dbo].[usp_SendStandardJobAlert]

CREATE   PROCEDURE dbo.usp_SendStandardJobAlert
(
      @JobName NVARCHAR(200)
    , @StepName NVARCHAR(200)
    , @Message NVARCHAR(MAX) = NULL
)
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @subject NVARCHAR(500) =
        '*** SQL ALERT: ' + @JobName + ' FAILED ***';

    DECLARE @body NVARCHAR(MAX) =
           'Job:      ' + @JobName + CHAR(13)
        +  'Step:     ' + @StepName + CHAR(13)
        +  'Status:   FAILED' + CHAR(13)
        +  'Server:   ' + @@SERVERNAME + CHAR(13)
        +  'Date:     ' + CONVERT(VARCHAR(30), GETDATE(), 120) + CHAR(13)
        +  CHAR(13)
        +  'Details:' + CHAR(13)
        +  ISNULL(@Message,'No additional information logged.') + CHAR(13);

    EXEC msdb.dbo.sp_send_dbmail
        @profile_name = 'DBAOpsProfile',
        @recipients   = 'DBAOps@YOURORG.gov; SQLServerAdminNotification@YOURORG.gov',
        @subject      = @subject,
        @body         = @body,
        @body_format  = 'TEXT';
END;

GO

-- [dbo].[usp_ServerInventory_Insert]
/********************************************************************
  usp_ServerInventory_Insert (Central Server)
********************************************************************/
CREATE   PROCEDURE dbo.usp_ServerInventory_Insert
(
    @ServerName            VARCHAR(200),
    @InstanceName          VARCHAR(200),
    @FQDN                  VARCHAR(300),
    @DomainName            VARCHAR(200),
    @Environment           VARCHAR(50),
    @AppCategory           VARCHAR(100),

    @SQLVersion            VARCHAR(50),
    @SQLVersionMajor       INT,
    @SQLBuild              VARCHAR(50),
    @SQLCumulativeUpdate   VARCHAR(50),
    @Edition               VARCHAR(200),
    @Collation             VARCHAR(200),
    @IsClustered           BIT,
    @IsAGNode              BIT,

    @CpuCount              INT,
    @MaxMemoryMB           INT,
    @TotalMemoryGB         DECIMAL(10,2),
    @NumTempDBFiles        INT,
    @MachineType           VARCHAR(20),

    @WindowsVersion        VARCHAR(200),
    @WindowsBuild          VARCHAR(50),
    @LastOSPatchDate       DATETIME,

    @InstanceStartTime     DATETIME,

    @CollectorStatus       VARCHAR(20),
    @ErrorMessage          VARCHAR(MAX)
)
AS
BEGIN
    SET NOCOUNT ON;

    INSERT INTO dbo.ServerInventory
    (
        ServerName, InstanceName, FQDN, DomainName, Environment, AppCategory,
        SQLVersion, SQLVersionMajor, SQLBuild, SQLCumulativeUpdate, Edition,
        Collation, IsClustered, IsAGNode,
        CpuCount, MaxMemoryMB, TotalMemoryGB, NumTempDBFiles, MachineType,
        WindowsVersion, WindowsBuild, LastOSPatchDate, InstanceStartTime,
        CollectorStatus, ErrorMessage, CollectionTime
    )
    VALUES
    (
        @ServerName, @InstanceName, @FQDN, @DomainName, @Environment, @AppCategory,
        @SQLVersion, @SQLVersionMajor, @SQLBuild, @SQLCumulativeUpdate, @Edition,
        @Collation, @IsClustered, @IsAGNode,
        @CpuCount, @MaxMemoryMB, @TotalMemoryGB, @NumTempDBFiles, @MachineType,
        @WindowsVersion, @WindowsBuild, @LastOSPatchDate, @InstanceStartTime,
        @CollectorStatus, @ErrorMessage, GETDATE()
    );
END

GO

-- [dbo].[usp_SyncVeeamRepositoryData]

CREATE PROCEDURE dbo.usp_SyncVeeamRepositoryData
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @RowsUpdated INT;
    
    -- Update BackupCompliance with Repository data from VeeamObjectMap
    UPDATE bc
    SET 
        bc.Repository = vom.Repository,
        bc.RepositoryVolume = ra.RepoVolume,
        bc.RepoTotalTB = ra.RepoTotalTB,
        bc.RepoFreeTB = ra.RepoFreeTB
    FROM ctl.BackupCompliance bc
    INNER JOIN config.VeeamObjectMap vom ON bc.ServerName = vom.ServerName
    INNER JOIN config.RepositoryAssignment ra ON vom.Repository = ra.RepositoryName
    WHERE vom.IsActive = 1 
      AND ra.IsActive = 1
      AND (bc.Repository IS NULL OR bc.Repository <> vom.Repository);
    
    SET @RowsUpdated = @@ROWCOUNT;
    
    -- Log the sync
    PRINT 'Veeam Repository Sync Complete: ' + CAST(@RowsUpdated AS VARCHAR(10)) + ' rows updated';
    
    -- Return summary
    SELECT 
        CASE 
            WHEN Repository IS NULL THEN 'NULL Repository'
            ELSE 'Has Repository'
        END AS Status,
        COUNT(*) AS RecordCount
    FROM ctl.BackupCompliance
    GROUP BY 
        CASE 
            WHEN Repository IS NULL THEN 'NULL Repository'
            ELSE 'Has Repository'
        END;
END;

GO

-- [dbo].[usp_UpdateBackupCompliance]

CREATE PROCEDURE dbo.usp_UpdateBackupCompliance
AS
BEGIN
    SET NOCOUNT ON;
    
    -- Update compliance based on SLA rules and CMS environment mapping
    UPDATE bc
    SET 
        bc.RPOHours = sla.MaxAgeHours,
        bc.NextExpectedBackupTime = DATEADD(HOUR, sla.MaxAgeHours, bc.LastBackupTime),
        bc.IsCompliant = CASE 
            WHEN bc.LastBackupTime IS NULL THEN 0
            WHEN DATEDIFF(HOUR, bc.LastBackupTime, GETDATE()) <= sla.MaxAgeHours THEN 1
            ELSE 0
        END,
        bc.Status = CASE 
            WHEN bc.LastBackupTime IS NULL THEN 'No Backup'
            WHEN DATEDIFF(HOUR, bc.LastBackupTime, GETDATE()) <= sla.MaxAgeHours THEN 'Compliant'
            WHEN DATEDIFF(HOUR, bc.LastBackupTime, GETDATE()) <= sla.MaxAgeHours * 1.5 THEN 'Warning'
            ELSE 'Critical'
        END
    FROM ctl.BackupCompliance bc
    INNER JOIN config.ServerInventory si ON bc.ServerName = si.ServerName
    INNER JOIN config.BackupSLARules sla ON sla.BackupType = 'Full' 
        AND sla.IsActive = 1
        AND sla.EnvironmentType = CASE 
            WHEN si.CMSEnvironment = 'Production' AND si.CMSGroupName = 'HIGH_CRITICALITY' THEN 'PROD-CRITICAL'
            WHEN si.CMSEnvironment = 'Production' THEN 'PROD'
            WHEN si.CMSEnvironment = 'Non-Production' THEN 'NON-PROD'
            WHEN si.CMSEnvironment = 'Infrastructure' THEN 'PROD'
            ELSE 'NON-PROD'
        END
    WHERE si.IsActive = 1;
    
    PRINT 'Backup Compliance Updated: ' + CAST(@@ROWCOUNT AS VARCHAR(10)) + ' rows';
END;

GO

-- [dbo].[usp_Verify_ServerInventory]

CREATE   PROCEDURE dbo.usp_Verify_ServerInventory
AS
BEGIN
    SET NOCOUNT ON;
    
    -- Verify server inventory from CMS
    SELECT 
        cms.ServerName,
        COALESCE(cms.Environment, cms.CMSEnvironment, 'Unknown') AS Environment,
        cms.SQLVersion,
        cms.IsActive,
        cms.LastSeen,
        cms.LastInventoryUpdate,
        CASE 
            WHEN cms.LastSeen >= DATEADD(DAY, -1, GETDATE()) THEN 'Current'
            WHEN cms.LastSeen >= DATEADD(DAY, -7, GETDATE()) THEN 'Recent'
            ELSE 'Stale'
        END AS InventoryStatus
    FROM config.ServerInventory cms
    WHERE (cms.ServerType = 'SQL Server' OR cms.ServerType IS NULL)
    ORDER BY 
        CASE WHEN cms.IsActive = 1 THEN 1 ELSE 2 END,
        cms.Environment, 
        cms.ServerName;
END

GO

-- [deployment].[usp_GenerateDeploymentScripts]

CREATE   PROCEDURE deployment.usp_GenerateDeploymentScripts
    @ServerName NVARCHAR(128) = NULL,  -- NULL = all servers
    @ComplianceTier VARCHAR(100) = NULL  -- NULL = all tiers
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @DeploymentID INT;
    DECLARE @ScriptCount INT = 0;
    
    -- Create deployment batch record
    INSERT INTO deployment.DeploymentBatches (DeploymentDate, DeployedBy, Status, ComplianceTier)
    VALUES (SYSDATETIME(), SYSTEM_USER, 'Generated', @ComplianceTier);
    
    SET @DeploymentID = SCOPE_IDENTITY();
    
    PRINT '=========================================================';
    PRINT 'GENERATING DEPLOYMENT SCRIPTS';
    PRINT '=========================================================';
    PRINT 'Deployment ID: ' + CAST(@DeploymentID AS VARCHAR(10));
    PRINT 'Tier Filter: ' + ISNULL(@ComplianceTier, 'ALL');
    PRINT 'Server Filter: ' + ISNULL(@ServerName, 'ALL');
    PRINT '';
    
    -- Generate script for each server with missing logins
    DECLARE @CurrentServer NVARCHAR(128);
    DECLARE @CurrentTier VARCHAR(100);
    DECLARE @ScriptText NVARCHAR(MAX);
    DECLARE @MissingCount INT;
    
    DECLARE server_cursor CURSOR FOR
    SELECT DISTINCT 
        ml.ServerName,
        ml.ComplianceTier,
        COUNT(*) AS MissingLogins
    FROM deployment.vw_MissingLogins ml
    WHERE (@ServerName IS NULL OR ml.ServerName = @ServerName)
      AND (@ComplianceTier IS NULL OR ml.ComplianceTier = @ComplianceTier)
    GROUP BY ml.ServerName, ml.ComplianceTier
    ORDER BY ml.ComplianceTier, ml.ServerName;
    
    OPEN server_cursor;
    FETCH NEXT FROM server_cursor INTO @CurrentServer, @CurrentTier, @MissingCount;
    
    WHILE @@FETCH_STATUS = 0
    BEGIN
        SET @ScriptText = '';
        
        -- Build header
        SET @ScriptText = @ScriptText + 
            '-- =========================================================' + CHAR(13) + CHAR(10) +
            '-- DBAOPS SECURITY DEPLOYMENT' + CHAR(13) + CHAR(10) +
            '-- Server: ' + @CurrentServer + CHAR(13) + CHAR(10) +
            '-- Tier: ' + @CurrentTier + CHAR(13) + CHAR(10) +
            '-- Generated: ' + CONVERT(VARCHAR(30), SYSDATETIME(), 120) + CHAR(13) + CHAR(10) +
            '-- Deployment ID: ' + CAST(@DeploymentID AS VARCHAR(10)) + CHAR(13) + CHAR(10) +
            '-- Missing Logins: ' + CAST(@MissingCount AS VARCHAR(10)) + CHAR(13) + CHAR(10) +
            '-- =========================================================' + CHAR(13) + CHAR(10) +
            '-- SAFETY FEATURES:' + CHAR(13) + CHAR(10) +
            '--   - Only CREATES logins, never DROPS anything' + CHAR(13) + CHAR(10) +
            '--   - All checks are idempotent (safe to run multiple times)' + CHAR(13) + CHAR(10) +
            '--   - Preserves existing logins and permissions' + CHAR(13) + CHAR(10) +
            '-- =========================================================' + CHAR(13) + CHAR(10) +
            CHAR(13) + CHAR(10) +
            'USE [master];' + CHAR(13) + CHAR(10) +
            'GO' + CHAR(13) + CHAR(10) +
            CHAR(13) + CHAR(10) +
            'PRINT ''========================================================='';' + CHAR(13) + CHAR(10) +
            'PRINT ''DBAOPS DEPLOYMENT TO: ' + @CurrentServer + ''';' + CHAR(13) + CHAR(10) +
            'PRINT ''========================================================='';' + CHAR(13) + CHAR(10) +
            'PRINT '''';' + CHAR(13) + CHAR(10) +
            CHAR(13) + CHAR(10);
        
        -- Add CREATE LOGIN scripts for missing logins
        SELECT @ScriptText = @ScriptText + 
            '-- --------------------------------------------------------' + CHAR(13) + CHAR(10) +
            '-- Create login: ' + ml.ADGroupName + CHAR(13) + CHAR(10) +
            '-- Access Level: ' + ml.AccessLevel + CHAR(13) + CHAR(10) +
            '-- Server Role: ' + ac.DesiredServerRole + CHAR(13) + CHAR(10) +
            '-- --------------------------------------------------------' + CHAR(13) + CHAR(10) +
            'IF NOT EXISTS (SELECT 1 FROM sys.server_principals WHERE name = ''' + ml.ADGroupName + ''')' + CHAR(13) + CHAR(10) +
            'BEGIN' + CHAR(13) + CHAR(10) +
            '    PRINT ''[' + CONVERT(VARCHAR(30), SYSDATETIME(), 120) + '] Creating login: ' + ml.ADGroupName + ''';' + CHAR(13) + CHAR(10) +
            '    CREATE LOGIN [' + ml.ADGroupName + '] FROM WINDOWS;' + CHAR(13) + CHAR(10) +
            '    PRINT ''    --> Login created successfully'';' + CHAR(13) + CHAR(10) +
            CHAR(13) + CHAR(10) +
            '    -- Add to server role: ' + ac.DesiredServerRole + CHAR(13) + CHAR(10) +
            '    PRINT ''    --> Adding to role: ' + ac.DesiredServerRole + ''';' + CHAR(13) + CHAR(10) +
            '    ALTER SERVER ROLE [' + ac.DesiredServerRole + '] ADD MEMBER [' + ml.ADGroupName + '];' + CHAR(13) + CHAR(10) +
            '    PRINT ''    --> Role membership assigned'';' + CHAR(13) + CHAR(10) +
            'END' + CHAR(13) + CHAR(10) +
            'ELSE' + CHAR(13) + CHAR(10) +
            'BEGIN' + CHAR(13) + CHAR(10) +
            '    PRINT ''[' + CONVERT(VARCHAR(30), SYSDATETIME(), 120) + '] Login already exists: ' + ml.ADGroupName + ' - Verifying role membership'';' + CHAR(13) + CHAR(10) +
            '    ' + CHAR(13) + CHAR(10) +
            '    -- Check if role membership exists' + CHAR(13) + CHAR(10) +
            '    IF NOT EXISTS (' + CHAR(13) + CHAR(10) +
            '        SELECT 1 FROM sys.server_role_members srm' + CHAR(13) + CHAR(10) +
            '        JOIN sys.server_principals r ON srm.role_principal_id = r.principal_id' + CHAR(13) + CHAR(10) +
            '        JOIN sys.server_principals m ON srm.member_principal_id = m.principal_id' + CHAR(13) + CHAR(10) +
            '        WHERE r.name = ''' + ac.DesiredServerRole + '''' + CHAR(13) + CHAR(10) +
            '          AND m.name = ''' + ml.ADGroupName + '''' + CHAR(13) + CHAR(10) +
            '    )' + CHAR(13) + CHAR(10) +
            '    BEGIN' + CHAR(13) + CHAR(10) +
            '        PRINT ''    --> Adding missing role membership: ' + ac.DesiredServerRole + ''';' + CHAR(13) + CHAR(10) +
            '        ALTER SERVER ROLE [' + ac.DesiredServerRole + '] ADD MEMBER [' + ml.ADGroupName + '];' + CHAR(13) + CHAR(10) +
            '        PRINT ''    --> Role membership assigned'';' + CHAR(13) + CHAR(10) +
            '    END' + CHAR(13) + CHAR(10) +
            '    ELSE' + CHAR(13) + CHAR(10) +
            '    BEGIN' + CHAR(13) + CHAR(10) +
            '        PRINT ''    --> Role membership already exists - No action needed'';' + CHAR(13) + CHAR(10) +
            '    END' + CHAR(13) + CHAR(10) +
            'END' + CHAR(13) + CHAR(10) +
            'GO' + CHAR(13) + CHAR(10) +
            CHAR(13) + CHAR(10)
        FROM deployment.vw_MissingLogins ml
        JOIN security.vw_ServerAccessControl ac ON 
            ml.ServerName = ac.ServerName AND 
            ml.ADGroupName = ac.ADGroupName
        WHERE ml.ServerName = @CurrentServer
        ORDER BY ml.ADGroupName;
        
        -- Add footer
        SET @ScriptText = @ScriptText +
            'PRINT '''';' + CHAR(13) + CHAR(10) +
            'PRINT ''========================================================='';' + CHAR(13) + CHAR(10) +
            'PRINT ''DEPLOYMENT COMPLETE FOR: ' + @CurrentServer + ''';' + CHAR(13) + CHAR(10) +
            'PRINT ''========================================================='';' + CHAR(13) + CHAR(10) +
            'PRINT '''';' + CHAR(13) + CHAR(10) +
            CHAR(13) + CHAR(10) +
            '-- Verification query:' + CHAR(13) + CHAR(10) +
            '/*' + CHAR(13) + CHAR(10) +
            'SELECT ' + CHAR(13) + CHAR(10) +
            '    sp.name AS LoginName,' + CHAR(13) + CHAR(10) +
            '    sp.type_desc AS LoginType,' + CHAR(13) + CHAR(10) +
            '    r.name AS ServerRole' + CHAR(13) + CHAR(10) +
            'FROM sys.server_principals sp' + CHAR(13) + CHAR(10) +
            'LEFT JOIN sys.server_role_members srm ON sp.principal_id = srm.member_principal_id' + CHAR(13) + CHAR(10) +
            'LEFT JOIN sys.server_principals r ON srm.role_principal_id = r.principal_id' + CHAR(13) + CHAR(10) +
            'WHERE sp.name LIKE ''%DBAOps%''' + CHAR(13) + CHAR(10) +
            'ORDER BY sp.name, r.name;' + CHAR(13) + CHAR(10) +
            '*/' + CHAR(13) + CHAR(10);
        
        -- Save to table
        INSERT INTO deployment.ServerDeploymentScripts (
            DeploymentID,
            ServerName,
            ScriptText,
            Status
        )
        VALUES (
            @DeploymentID,
            @CurrentServer,
            @ScriptText,
            'Ready'
        );
        
        SET @ScriptCount = @ScriptCount + 1;
        PRINT 'Generated script for: ' + @CurrentServer + ' (' + CAST(@MissingCount AS VARCHAR(10)) + ' logins)';
        
        FETCH NEXT FROM server_cursor INTO @CurrentServer, @CurrentTier, @MissingCount;
    END
    
    CLOSE server_cursor;
    DEALLOCATE server_cursor;
    
    -- Summary
    PRINT '';
    PRINT '=========================================================';
    PRINT 'GENERATION COMPLETE';
    PRINT '=========================================================';
    PRINT 'Deployment ID: ' + CAST(@DeploymentID AS VARCHAR(10));
    PRINT 'Scripts Generated: ' + CAST(@ScriptCount AS VARCHAR(10));
    PRINT '';
    PRINT 'NEXT STEPS:';
    PRINT '1. Review scripts:';
    PRINT '   SELECT * FROM deployment.ServerDeploymentScripts WHERE DeploymentID = ' + CAST(@DeploymentID AS VARCHAR(10));
    PRINT '';
    PRINT '2. Export script for a specific server:';
    PRINT '   SELECT ScriptText FROM deployment.ServerDeploymentScripts';
    PRINT '   WHERE DeploymentID = ' + CAST(@DeploymentID AS VARCHAR(10)) + ' AND ServerName = ''YOUR_SERVER'';';
    PRINT '';
    PRINT '3. Test deployment on Tier 4 servers first';
    PRINT '4. Then deploy to Tier 3, 2, and 1';
    PRINT '';
    
    -- Return summary
    SELECT 
        @DeploymentID AS DeploymentID,
        @ScriptCount AS ScriptsGenerated,
        'Ready for deployment' AS Status;
END;

GO

-- [etl].[Cleanup_VeeamBackups]

CREATE PROCEDURE etl.Cleanup_VeeamBackups
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE 
        @CutoffDate DATETIME = DATEADD(DAY, -30, GETDATE()),
        @RowsDeleted BIGINT = 0;

    BEGIN TRY
        DELETE FROM etl.VeeamBackups
        WHERE LoadDate < @CutoffDate;

        SET @RowsDeleted = @@ROWCOUNT;

        INSERT INTO audit.DataCleanupLog
        (
            CleanupProcedure,
            RowsDeleted,
            CutoffDate,
            RunDate
        )
        VALUES
        (
            'etl.Cleanup_VeeamBackups',
            @RowsDeleted,
            @CutoffDate,
            GETDATE()
        );
    END TRY
    BEGIN CATCH
        DECLARE @Err NVARCHAR(4000) = ERROR_MESSAGE();
        RAISERROR('Cleanup failed: %s', 16, 1, @Err);
        RETURN;
    END CATCH;
END;

GO

-- [etl].[IngestVeeamJsonReport]
CREATE OR ALTER PROCEDURE etl.IngestVeeamJsonReport
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @json NVARCHAR(MAX);

    -- Load JSON from file using OPENROWSET
    SELECT @json = BulkColumn
    FROM OPENROWSET(
            BULK 'C:\VeeamReports\HourlyBackupJobReport.json',
            SINGLE_CLOB
        ) AS j;

    -- Save raw JSON into staging table
    INSERT INTO staging.VeeamJobReportJson (JsonContent)
    VALUES (@json);

    -- Parse JSON into fact table
    INSERT INTO fact.VeeamJobHistory
    (
        JobName, JobType, BackupType, Progress, Status,
        StartTime, EndTime, Result
    )
    SELECT 
        j.JobName,
        j.JobType,
        j.BackupType,
        j.Progress,
        j.Status,
        j.StartTime,
        j.EndTime,
        j.Result
    FROM OPENJSON(@json)
    WITH (
        JobName     VARCHAR(200),
        JobType     VARCHAR(50),
        BackupType  VARCHAR(50),
        Progress    VARCHAR(50),
        Status      VARCHAR(50),
        StartTime   DATETIME2,
        EndTime     DATETIME2,
        Result      VARCHAR(50)
    ) j;

END;

GO

-- [etl].[Load_VeeamBackups_FromJSON]

CREATE PROCEDURE etl.Load_VeeamBackups_FromJSON
    @JsonFilePath NVARCHAR(4000) = N'C:\VeeamReports\HourlyBackupJobReport.json'
AS
BEGIN
    SET NOCOUNT ON;
    SET XACT_ABORT ON;

    BEGIN TRY
        BEGIN TRANSACTION;

        DECLARE @Json NVARCHAR(MAX);
        DECLARE @SqlCmd NVARCHAR(MAX);
        DECLARE @ErrorMessage NVARCHAR(4000), @ErrorSeverity INT, @ErrorState INT, @ErrorNumber INT;

        -----------------------------------------------------
        -- Step 1: Load JSON into NVARCHAR(MAX)
        -----------------------------------------------------
        SET @SqlCmd = N'
            SELECT @JsonData = BulkColumn
            FROM OPENROWSET(
                    BULK ''' + REPLACE(@JsonFilePath, '''', '''''') + N''',
                    SINGLE_CLOB
                ) AS x;';

        EXEC sp_executesql
            @stmt = @SqlCmd,
            @params = N'@JsonData NVARCHAR(MAX) OUTPUT',
            @JsonData = @Json OUTPUT;

        IF @Json IS NULL OR LTRIM(RTRIM(@Json)) = ''
            THROW 50001, 'JSON file was empty or unreadable.', 1;

        -----------------------------------------------------
        -- Step 2: Stage and convert epoch timestamps
        -----------------------------------------------------
        IF OBJECT_ID('tempdb..#RawStaging') IS NOT NULL DROP TABLE #RawStaging;

        CREATE TABLE #RawStaging (
            JobName     NVARCHAR(255),
            JobType     NVARCHAR(100),
            BackupType  NVARCHAR(100),
            Progress    INT,
            Status      NVARCHAR(100),
            StartEpoch  BIGINT,
            EndEpoch    BIGINT,
            Result      NVARCHAR(50)
        );

        INSERT INTO #RawStaging
        SELECT 
            JobName,
            JobType,
            BackupType,
            Progress,
            Status,
            CAST(REPLACE(REPLACE(StartTime, '/Date(', ''), ')/', '') AS BIGINT),
            CAST(REPLACE(REPLACE(EndTime,   '/Date(', ''), ')/', '') AS BIGINT),
            Result
        FROM OPENJSON(@Json)
        WITH (
            JobName     NVARCHAR(255) '$.JobName',
            JobType     NVARCHAR(100) '$.JobType',
            BackupType  NVARCHAR(100) '$.BackupType',
            Progress    INT           '$.Progress',
            Status      NVARCHAR(100) '$.Status',
            StartTime   NVARCHAR(50)  '$.StartTime',
            EndTime     NVARCHAR(50)  '$.EndTime',
            Result      NVARCHAR(50)  '$.Result'
        );

        -----------------------------------------------------
        -- Step 3: Insert into permanent table
        -----------------------------------------------------
        INSERT INTO etl.VeeamBackups
        (
            JobName, JobType, BackupType, Progress, Status,
            StartTime, EndTime, Result
        )
        SELECT 
            JobName, JobType, BackupType, Progress, Status,
            DATEADD(MILLISECOND, StartEpoch % 1000, DATEADD(SECOND, StartEpoch / 1000, '1970-01-01')),
            DATEADD(MILLISECOND, EndEpoch % 1000, DATEADD(SECOND, EndEpoch / 1000, '1970-01-01')),
            Result
        FROM #RawStaging;

        COMMIT TRANSACTION;
    END TRY
    BEGIN CATCH
        IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;

        SELECT 
            @ErrorNumber = ERROR_NUMBER(),
            @ErrorMessage = ERROR_MESSAGE(),
            @ErrorSeverity = ERROR_SEVERITY(),
            @ErrorState = ERROR_STATE();

        THROW @ErrorNumber, @ErrorMessage, @ErrorState;
    END CATCH

    IF OBJECT_ID('tempdb..#RawStaging') IS NOT NULL DROP TABLE #RawStaging;
END;

GO

-- [etl].[usp_Aggregate_ServerPerformanceDaily]
CREATE OR ALTER PROCEDURE etl.usp_Aggregate_ServerPerformanceDaily
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @LoadDate date = DATEADD(DAY, -1, CAST(GETDATE() AS date));

    INSERT INTO DBAOps.ServerPerformanceDaily
    (
        ServerName,
        LoadDate,
        AvgCPUPercent,
        MaxCPUPercent,
        AvgMemoryPercent,
        MaxMemoryPercent,
        AvgDiskLatencyMs,
        MaxDiskLatencyMs
    )
    SELECT
        ServerName,
        @LoadDate,
        AVG(ISNULL(CPUPercent,0.0)),
        MAX(ISNULL(CPUPercent,0.0)),
        AVG(ISNULL(MemoryPercent,0.0)),
        MAX(ISNULL(MemoryPercent,0.0)),
        AVG(ISNULL(DiskLatencyMs,0.0)),
        MAX(ISNULL(DiskLatencyMs,0.0))
    FROM DBAOps.PerformanceTelemetry
    WHERE CAST(CaptureTime AS date) = @LoadDate
    GROUP BY ServerName;
END;

GO

-- [etl].[usp_AggregatePerformanceDaily]

CREATE   PROCEDURE etl.usp_AggregatePerformanceDaily
AS
BEGIN
    /*
    Wrapper procedure - calls the actual implementation
    Created: 2025-12-19 to fix job failure
    */
    SET NOCOUNT ON;
    
    -- Call the actual procedure in DBAOps schema
    EXEC DBAOps.usp_AggregateServerPerformanceDaily;
END

GO

-- [etl].[usp_ApplyDataRetention]

CREATE PROCEDURE etl.usp_ApplyDataRetention
    @DryRun BIT = 1
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @TableName NVARCHAR(128);
    DECLARE @RetentionDays INT;
    DECLARE @SQL NVARCHAR(MAX);
    DECLARE @CutoffDate DATETIME;
    DECLARE @RowsAffected INT;
    
    PRINT '=== Data Retention Execution ==='
    PRINT 'Mode: ' + CASE WHEN @DryRun = 1 THEN 'DRY RUN' ELSE 'LIVE' END;
    PRINT 'Time: ' + CONVERT(VARCHAR, GETDATE(), 120);
    PRINT ''
    
    DECLARE retention_cursor CURSOR FOR
        SELECT TableName, RetentionDays
        FROM config.DataRetention
        WHERE RetentionDays > 0;
    
    OPEN retention_cursor;
    FETCH NEXT FROM retention_cursor INTO @TableName, @RetentionDays;
    
    WHILE @@FETCH_STATUS = 0
    BEGIN
        SET @CutoffDate = DATEADD(DAY, -@RetentionDays, GETDATE());
        
        PRINT 'Table: ' + @TableName + ' (Retention: ' + CAST(@RetentionDays AS VARCHAR) + ' days)';
        PRINT '  Cutoff: ' + CONVERT(VARCHAR, @CutoffDate, 120);
        
        -- Count rows to delete
        SET @SQL = 'SELECT @Cnt = COUNT(*) FROM ' + @TableName + ' WHERE LoadDate < @Cutoff';
        BEGIN TRY
            EXEC sp_executesql @SQL, N'@Cutoff DATETIME, @Cnt INT OUTPUT', @CutoffDate, @RowsAffected OUTPUT;
            PRINT '  Rows to remove: ' + CAST(ISNULL(@RowsAffected, 0) AS VARCHAR);
            
            IF @DryRun = 0 AND ISNULL(@RowsAffected, 0) > 0
            BEGIN
                SET @SQL = 'DELETE FROM ' + @TableName + ' WHERE LoadDate < @Cutoff';
                EXEC sp_executesql @SQL, N'@Cutoff DATETIME', @CutoffDate;
                PRINT '  DELETED: ' + CAST(@@ROWCOUNT AS VARCHAR) + ' rows';
                
                UPDATE config.DataRetention SET LastCleanupDate = GETDATE() WHERE TableName = @TableName;
            END
        END TRY
        BEGIN CATCH
            PRINT '  ERROR: ' + ERROR_MESSAGE();
        END CATCH
        
        PRINT ''
        FETCH NEXT FROM retention_cursor INTO @TableName, @RetentionDays;
    END
    
    CLOSE retention_cursor;
    DEALLOCATE retention_cursor;
    
    PRINT '=== Retention Complete ==='
END

GO

-- [etl].[usp_ArchiveOldJobHistory]

CREATE PROCEDURE etl.usp_ArchiveOldJobHistory
    @RetentionDays INT = 365
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @ArchiveCutoff DATETIME2 = DATEADD(DAY, -@RetentionDays, GETDATE());
    DECLARE @RowsDeleted INT;
    
    -- Delete records older than retention period
    DELETE FROM etl.fact_JobExecutionHistory
    WHERE CAST(CAST(RunDate AS VARCHAR(8)) + ' ' + 
          STUFF(STUFF(RIGHT('000000' + CAST(RunTime AS VARCHAR(6)), 6), 5, 0, ':'), 3, 0, ':')
          AS DATETIME2) < @ArchiveCutoff;
    
    SET @RowsDeleted = @@ROWCOUNT;
    
    PRINT 'Archived ' + CAST(@RowsDeleted AS VARCHAR(10)) + ' job history records older than ' + 
          CAST(@RetentionDays AS VARCHAR(10)) + ' days';
    
    RETURN @RowsDeleted;
END;

GO

-- [etl].[usp_CleanupRetention]

CREATE PROCEDURE etl.usp_CleanupRetention
    @DryRun BIT = 0  -- Set to 1 to preview without deleting
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @SchemaName VARCHAR(50), @TableName VARCHAR(100), @DateColumn VARCHAR(50);
    DECLARE @RetentionDays INT, @SQL NVARCHAR(MAX), @RowsDeleted INT;
    DECLARE @CutoffDate DATETIME;
    
    DECLARE retention_cursor CURSOR FOR
        SELECT SchemaName, TableName, DateColumn, RetentionDays
        FROM config.DataRetention
        WHERE IsActive = 1;
    
    OPEN retention_cursor;
    FETCH NEXT FROM retention_cursor INTO @SchemaName, @TableName, @DateColumn, @RetentionDays;
    
    WHILE @@FETCH_STATUS = 0
    BEGIN
        SET @CutoffDate = DATEADD(DAY, -@RetentionDays, GETDATE());
        
        -- Check if table exists
        IF OBJECT_ID(@SchemaName + '.' + @TableName, 'U') IS NOT NULL
        BEGIN
            IF @DryRun = 1
            BEGIN
                -- Preview mode
                SET @SQL = 'SELECT ''' + @SchemaName + '.' + @TableName + ''' AS TableName, ' +
                           'COUNT(*) AS RowsToDelete ' +
                           'FROM ' + QUOTENAME(@SchemaName) + '.' + QUOTENAME(@TableName) + ' ' +
                           'WHERE ' + QUOTENAME(@DateColumn) + ' < ''' + CONVERT(VARCHAR, @CutoffDate, 120) + '''';
                EXEC sp_executesql @SQL;
            END
            ELSE
            BEGIN
                -- Delete old records
                SET @SQL = 'DELETE FROM ' + QUOTENAME(@SchemaName) + '.' + QUOTENAME(@TableName) + ' ' +
                           'WHERE ' + QUOTENAME(@DateColumn) + ' < ''' + CONVERT(VARCHAR, @CutoffDate, 120) + '''';
                EXEC sp_executesql @SQL;
                SET @RowsDeleted = @@ROWCOUNT;
                
                -- Update tracking
                UPDATE config.DataRetention
                SET LastCleanupDate = SYSDATETIME(),
                    LastRowsDeleted = @RowsDeleted
                WHERE SchemaName = @SchemaName AND TableName = @TableName;
                
                PRINT 'Cleaned ' + @SchemaName + '.' + @TableName + ': ' + CAST(@RowsDeleted AS VARCHAR) + ' rows deleted';
            END
        END
        ELSE
            PRINT 'Table not found: ' + @SchemaName + '.' + @TableName;
        
        FETCH NEXT FROM retention_cursor INTO @SchemaName, @TableName, @DateColumn, @RetentionDays;
    END
    
    CLOSE retention_cursor;
    DEALLOCATE retention_cursor;
END;

GO

-- [etl].[usp_CollectBackupTelemetry]

CREATE PROCEDURE etl.usp_CollectBackupTelemetry
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @Inserted INT = 0;
    
    -- Insert into DBAOps.BackupTelemetry
    -- Available columns: BackupTelemetryID, ServerName, DatabaseName, BackupType,
    --                    BackupStartDate, BackupFinishDate, BackupSizeMB, CompressedSizeMB,
    --                    BackupPath, IsSuccessful, ErrorMessage, RecoveryModel, CollectionTime
    -- NOTE: BackupDurationS does NOT exist - removed from INSERT
    
    INSERT INTO DBAOps.BackupTelemetry (
        ServerName, DatabaseName, BackupType, BackupStartDate, BackupFinishDate,
        BackupSizeMB, CompressedSizeMB, BackupPath,
        IsSuccessful, ErrorMessage, RecoveryModel, CollectionTime
    )
    SELECT 
        @@SERVERNAME AS ServerName,
        bs.database_name AS DatabaseName,
        CASE bs.type 
            WHEN 'D' THEN 'Full'
            WHEN 'I' THEN 'Differential'
            WHEN 'L' THEN 'Log'
            ELSE bs.type
        END AS BackupType,
        bs.backup_start_date AS BackupStartDate,
        bs.backup_finish_date AS BackupFinishDate,
        CAST(bs.backup_size / 1048576.0 AS DECIMAL(18,2)) AS BackupSizeMB,
        CAST(ISNULL(bs.compressed_backup_size, bs.backup_size) / 1048576.0 AS DECIMAL(18,2)) AS CompressedSizeMB,
        bmf.physical_device_name AS BackupPath,
        1 AS IsSuccessful,
        NULL AS ErrorMessage,
        d.recovery_model_desc AS RecoveryModel,
        SYSDATETIME() AS CollectionTime
    FROM msdb.dbo.backupset bs
    INNER JOIN sys.databases d ON bs.database_name = d.name
    LEFT JOIN msdb.dbo.backupmediafamily bmf ON bs.media_set_id = bmf.media_set_id
    WHERE bs.backup_start_date > DATEADD(DAY, -1, GETDATE())
      AND NOT EXISTS (
          SELECT 1 FROM DBAOps.BackupTelemetry bt
          WHERE bt.ServerName = @@SERVERNAME
            AND bt.DatabaseName = bs.database_name
            AND bt.BackupStartDate = bs.backup_start_date
      );
    
    SET @Inserted = @@ROWCOUNT;
    
    -- Log the operation
    INSERT INTO log.AutomationLog (ModuleName, RunStatus, Message, ServerName, RowsAffected, LogType, LogTime)
    VALUES ('CollectBackupTelemetry', 'Success', 'Collected ' + CAST(@Inserted AS VARCHAR(10)) + ' backup records',
            @@SERVERNAME, @Inserted, 'INFO', SYSDATETIME());
    
    PRINT 'Backup telemetry collected: ' + CAST(@Inserted AS VARCHAR(10)) + ' records';
    
    SELECT @Inserted AS RecordsCollected;
END;

GO

-- [etl].[usp_Load_VeeamBackupCompliance_Staging]
CREATE OR ALTER PROCEDURE etl.usp_Load_VeeamBackupCompliance_Staging
AS
BEGIN
    SET NOCOUNT ON;

    TRUNCATE TABLE etl.VeeamBackupCompliance_Staging;

    INSERT INTO etl.VeeamBackupCompliance_Staging
    (
        BackupID, VmName, JobName, BackupType, EndTime, LoadDate,
        BackupSizeGB, Repository, RepositoryVolume, RepoTotalTB,
        RepoFreeTB, RepoFreePercent, ExpectedFrequency, IsCompliant
    )
    SELECT
        vb.BackupID,
        vb.JobName AS VmName,              -- Assign VM name = job name for now
        vb.JobName,
        vb.BackupType,
        vb.EndTime,
        vb.LoadDate,
        NULL, NULL, NULL, NULL, NULL, NULL,   -- repository metrics not available yet
        'Daily',
        CASE WHEN DATEDIFF(HOUR, vb.EndTime, GETDATE()) <= 24 THEN 1 ELSE 0 END
    FROM etl.VeeamBackups vb
    WHERE vb.EndTime IS NOT NULL;
END

GO

-- [etl].[usp_ProcessVeeamBackups]
CREATE OR ALTER PROCEDURE etl.usp_ProcessVeeamBackups
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @Now DATETIME = GETDATE();
    
    -- Get the most recent backup per server from etl.VeeamBackups
    ;WITH LatestBackups AS (
        SELECT 
            JobName AS ServerName,
            JobName AS VmName,
            'Unknown' AS DatabaseName,
            COALESCE(BackupType, 'Unknown') AS BackupType,
            MAX(EndTime) AS LastBackupTime,
            MAX(LoadDate) AS CollectionTime,
            JobName AS JobName,
            'Veeam' AS BackupSource,
            'Unknown' AS BackupMode
        FROM etl.VeeamBackups
        WHERE EndTime IS NOT NULL
          AND EndTime > DATEADD(DAY, -14, @Now)  -- Only last 14 days
        GROUP BY JobName, BackupType
    )
    
    -- Merge into ctl.BackupCompliance
    MERGE ctl.BackupCompliance AS T
    USING LatestBackups AS S
    ON T.ServerName = S.ServerName 
       AND ISNULL(T.BackupType, 'Unknown') = ISNULL(S.BackupType, 'Unknown')
    
    WHEN MATCHED THEN
        UPDATE SET
            T.LastBackupTime = S.LastBackupTime,
            T.CollectionTime = @Now,
            T.VmName = S.VmName,
            T.JobName = S.JobName,
            T.BackupSource = S.BackupSource
    
    WHEN NOT MATCHED THEN
        INSERT (ServerName, VmName, DatabaseName, BackupType, LastBackupTime, 
                CollectionTime, JobName, BackupSource, BackupMode)
        VALUES (S.ServerName, S.VmName, S.DatabaseName, S.BackupType, S.LastBackupTime,
                @Now, S.JobName, S.BackupSource, S.BackupMode);
    
    DECLARE @Merged INT = @@ROWCOUNT;
    PRINT 'Processed ' + CAST(@Merged AS VARCHAR) + ' Veeam backup records';
    
    -- Now recalculate compliance scores
    EXEC ctl.usp_Recalculate_BackupCompliance;
    
END;

GO

-- [etl].[usp_RefreshDashboardMetrics]

CREATE   PROCEDURE etl.usp_RefreshDashboardMetrics
AS
BEGIN
    SET NOCOUNT ON;
    
    -- Refresh metrics using CMS as source
    SELECT 
        COUNT(*) AS TotalServers,
        SUM(CASE WHEN COALESCE(Environment, CMSEnvironment) = 'Production' THEN 1 ELSE 0 END) AS ProdServers,
        SUM(CASE WHEN IsActive = 1 THEN 1 ELSE 0 END) AS ActiveServers
    FROM config.ServerInventory
    WHERE (ServerType = 'SQL Server' OR ServerType IS NULL);
END

GO

-- [fact].[usp_CalculateRiskScore]
CREATE OR ALTER PROCEDURE fact.usp_CalculateRiskScore
AS
BEGIN
    INSERT INTO fact.ServerRiskScore (ServerName, MaxDOPScore, CTFPScore, MemoryScore, PatchScore, DiskScore, TotalScore)
    SELECT 
        s.ServerName,
        CASE WHEN s.MaxDOP BETWEEN 8 AND 100 THEN 20 ELSE 0 END AS MaxDOPScore,
        CASE WHEN s.CostThreshold >= 35 THEN 20 ELSE 0 END AS CTFPScore,
        CASE WHEN s.MaxMemoryMB IS NOT NULL AND s.MaxMemoryMB > 0 THEN 20 ELSE 0 END AS MemoryScore,
        CASE WHEN DATEDIFF(DAY, s.LastPatchDate, GETDATE()) <= 365 THEN 20 ELSE 0 END AS PatchScore,
        CASE WHEN d.PercentFree >= 20 THEN 20 ELSE 0 END AS DiskScore,
        (
            CASE WHEN s.MaxDOP BETWEEN 8 AND 100 THEN 20 ELSE 0 END +
            CASE WHEN s.CostThreshold >= 35 THEN 20 ELSE 0 END +
            CASE WHEN s.MaxMemoryMB IS NOT NULL AND s.MaxMemoryMB > 0 THEN 20 ELSE 0 END +
            CASE WHEN DATEDIFF(DAY, s.LastPatchDate, GETDATE()) <= 365 THEN 20 ELSE 0 END +
            CASE WHEN d.PercentFree >= 20 THEN 20 ELSE 0 END
        ) AS TotalScore
    FROM config.ServerInventory s
    LEFT JOIN (
        SELECT ServerName, MIN(PercentFree) AS PercentFree
        FROM fact.DiskSpaceHistory
        GROUP BY ServerName
    ) d ON s.ServerName = d.ServerName;
END;

GO

-- [fact].[usp_Collect_BackupCompliance]

-- ============================================================================
-- STEP 2: Create missing procedure fact.usp_Collect_BackupCompliance
-- ============================================================================
CREATE   PROCEDURE fact.usp_Collect_BackupCompliance
AS
BEGIN
    SET NOCOUNT ON;
    
    -- This procedure is a stub - backup compliance is handled by alert.usp_BackupComplianceDailyReport
    -- Just update the existing ctl.BackupCompliance table with latest data
    
    PRINT 'Backup compliance collection - using existing ctl.BackupCompliance data';
    
    -- Return count
    SELECT COUNT(*) AS BackupRecords FROM ctl.BackupCompliance;
END;

GO

-- [fact].[usp_Collect_DBAOpsMetrics]

-- Create or update the main collector procedure with error handling
CREATE   PROCEDURE fact.usp_Collect_DBAOpsMetrics
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @StartTime DATETIME = GETDATE();
    DECLARE @ErrorCount INT = 0;
    
    PRINT 'Starting DBAOps Metrics Collection at ' + CONVERT(VARCHAR, @StartTime, 120);
    
    -- Backup Compliance
    BEGIN TRY
        PRINT 'Running Backup Compliance Collector...';
        IF OBJECT_ID('fact.usp_Collect_BackupCompliance', 'P') IS NOT NULL
            EXEC fact.usp_Collect_BackupCompliance;
        ELSE
            PRINT 'Skipped - procedure not found';
    END TRY
    BEGIN CATCH
        SET @ErrorCount += 1;
        PRINT 'Error in Backup Compliance: ' + ERROR_MESSAGE();
    END CATCH
    
    -- Job Compliance
    BEGIN TRY
        PRINT 'Running Job Compliance Collector...';
        IF OBJECT_ID('fact.JobComplianceLog', 'U') IS NOT NULL
        BEGIN
            -- Clear old data
            DELETE FROM fact.JobComplianceLog WHERE CaptureTime < DATEADD(DAY, -7, GETDATE());
            PRINT 'Job compliance log cleaned';
        END
        ELSE
            PRINT 'Skipped - table not found';
    END TRY
    BEGIN CATCH
        SET @ErrorCount += 1;
        PRINT 'Error in Job Compliance: ' + ERROR_MESSAGE();
    END CATCH
    
    -- Summary
    PRINT '';
    PRINT '=== Collection Complete ===';
    PRINT 'End Time: ' + CONVERT(VARCHAR, GETDATE(), 120);
    PRINT 'Errors: ' + CAST(@ErrorCount AS VARCHAR);
    
    IF @ErrorCount > 0
        RAISERROR('Collection completed with %d error(s)', 16, 1, @ErrorCount);
END;

GO

-- [fact].[usp_Collect_JobCompliance]

CREATE   PROCEDURE fact.usp_Collect_JobCompliance
AS
BEGIN
    SET NOCOUNT ON;
    
    SELECT 
        cms.ServerName,
        COALESCE(cms.Environment, cms.CMSEnvironment, 'Unknown') AS Environment,
        cms.ComplianceTier
    FROM config.ServerInventory cms
    WHERE cms.IsActive = 1
    AND (cms.ServerType = 'SQL Server' OR cms.ServerType IS NULL)
    ORDER BY cms.Environment, cms.ServerName;
END

GO

-- [fact].[usp_Collect_LoginStatus]

CREATE PROCEDURE fact.usp_Collect_LoginStatus
AS
BEGIN
    SET NOCOUNT ON;

    INSERT INTO audit.LoginChar (EventTime, HostName, LoginName, AuditID, Status)
    SELECT 
        GETDATE(),
        CAST(SERVERPROPERTY('MachineName') AS VARCHAR(200)),
        name,
        1,
        CASE WHEN is_disabled = 1 THEN 'Disabled' ELSE 'Active' END
    FROM sys.sql_logins
    WHERE name NOT IN ('sa','public');
END;

GO

-- [fact].[usp_Collect_ServiceStatus]

CREATE PROCEDURE fact.usp_Collect_ServiceStatus
AS
BEGIN
    SET NOCOUNT ON;

    INSERT INTO audit.AuditLog (
        EventTime, LoginName, HostName, ActionType, ObjectName, ObjectType, TSQLCommand
    )
    SELECT
        GETDATE(),
        SUSER_SNAME(),
        CAST(SERVERPROPERTY('MachineName') AS VARCHAR(200)),
        'INFO',
        servicename,
        'SERVICE',
        status_desc
    FROM sys.dm_server_services;
END;

GO

-- [fact].[usp_CollectCPUPerformance]

-- Create CPU Collection Procedure
CREATE   PROCEDURE fact.usp_CollectCPUPerformance
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @Now DATETIME2 = GETDATE();
    DECLARE @DateKey INT = CONVERT(INT, CONVERT(VARCHAR, @Now, 112));
    DECLARE @TimeKey INT = DATEPART(HOUR, @Now) * 60 + DATEPART(MINUTE, @Now);
    
    DECLARE @Server NVARCHAR(128), @ServerKey INT;
    DECLARE @SQL NVARCHAR(MAX);
    
    DECLARE server_cursor CURSOR FOR
        SELECT s.ServerKey, s.Server
        FROM dim.DimServer s
        WHERE s.IsCurrent = 1;
    
    OPEN server_cursor;
    FETCH NEXT FROM server_cursor INTO @ServerKey, @Server;
    
    WHILE @@FETCH_STATUS = 0
    BEGIN
        BEGIN TRY
            SET @SQL = '
                SELECT 
                    ' + CAST(@DateKey AS VARCHAR) + ',
                    ' + CAST(@TimeKey AS VARCHAR) + ',
                    ' + CAST(@ServerKey AS VARCHAR) + ',
                    GETDATE(),
                    SQLProcessUtilization,
                    SystemIdle,
                    100 - SystemIdle - SQLProcessUtilization
                FROM (
                    SELECT TOP 1
                        record.value(''(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]'', ''int'') AS SystemIdle,
                        record.value(''(./Record/SchedulerMonitorEvent/SystemHealth/ProcessUtilization)[1]'', ''int'') AS SQLProcessUtilization
                    FROM (
                        SELECT CONVERT(XML, record) AS record
                        FROM [' + @Server + '].master.sys.dm_os_ring_buffers
                        WHERE ring_buffer_type = N''RING_BUFFER_SCHEDULER_MONITOR''
                          AND record LIKE ''%<SystemHealth>%''
                    ) AS x
                    ORDER BY record.value(''(./Record/@id)[1]'', ''int'') DESC
                ) cpu';
            
            INSERT INTO fact.CPUPerformance (DateKey, TimeKey, ServerKey, CollectionTime, SQLServerCPUPercent, SystemIdleCPUPercent, OtherProcessCPUPercent)
            EXEC sp_executesql @SQL;
            
        END TRY
        BEGIN CATCH
            -- Log error but continue
            PRINT 'Error collecting from ' + @Server + ': ' + ERROR_MESSAGE();
        END CATCH
        
        FETCH NEXT FROM server_cursor INTO @ServerKey, @Server;
    END
    
    CLOSE server_cursor;
    DEALLOCATE server_cursor;
    
    PRINT 'CPU collection complete: ' + CAST(@@ROWCOUNT AS VARCHAR) + ' servers';
END;

GO

-- [fact].[usp_InsertMaxDOPCompliance]
CREATE OR ALTER PROCEDURE fact.usp_InsertMaxDOPCompliance
    @SqlInstance VARCHAR(128),
    @MaxDOP INT,
    @CostThreshold INT
AS
BEGIN
    DECLARE @IsCompliant BIT = CASE WHEN @CostThreshold >= 35 AND @CostThreshold <= 100 THEN 1 ELSE 0 END;

    INSERT INTO fact.MaxDOPCompliance(SqlInstance, MaxDOP, CostThreshold, IsCompliant, Notes)
    VALUES(@SqlInstance, @MaxDOP, @CostThreshold, @IsCompliant,
           CASE WHEN @IsCompliant = 0 THEN 'Non-Compliant Threshold' ELSE 'OK' END);
END

GO

-- [fact].[usp_Load_BackupCompliance]
CREATE OR ALTER PROCEDURE fact.usp_Load_BackupCompliance  
AS  
BEGIN  
    SET NOCOUNT ON;  
  
    BEGIN TRY  
        -- Delete existing records for servers we're about to update
        DELETE f
        FROM fact.BackupCompliance f
        WHERE EXISTS (
            SELECT 1 FROM ctl.BackupCompliance c
            WHERE c.ServerName = f.ServerName
              AND ISNULL(c.BackupType, 'Unknown') = ISNULL(f.BackupType, 'Unknown')
        );

        -- Insert fresh data
        INSERT INTO fact.BackupCompliance
        (
            ServerName, DatabaseName, BackupType,
            LastBackupTime, AgeMinutes, SLA_Minutes, Status,
            InsertTime, LoadDate, BackupSource, VmName,
            NextExpectedBackupTime, JobName, BackupMode,
            BackupSizeGB, Repository, RepositoryVolume,
            RepoTotalTB, RepoFreeTB, ExpectedFrequency, IsCompliant
        )
        SELECT
            S.ServerName, S.DatabaseName, S.BackupType,
            S.LastBackupTime, S.LogAgeMinutes, S.FullAgeHours,
            S.Status, GETDATE(), GETDATE(), S.BackupSource, S.VmName,
            S.NextExpectedBackupTime, S.JobName, S.BackupMode,
            S.BackupSizeGB, S.Repository, S.RepositoryVolume,
            S.RepoTotalTB, S.RepoFreeTB, S.ExpectedFrequency, S.IsCompliant
        FROM ctl.BackupCompliance S;

        PRINT 'Loaded ' + CAST(@@ROWCOUNT AS VARCHAR) + ' records to fact.BackupCompliance';

    END TRY  
    BEGIN CATCH  
        INSERT INTO ctl.ETLErrorLog (ProcedureName, ErrorMessage, ErrorLine, LogDate)
        VALUES ('fact.usp_Load_BackupCompliance', ERROR_MESSAGE(), ERROR_LINE(), GETDATE());
    END CATCH;  
END;

GO

-- [fact].[usp_PopulateDatabaseStatus]

CREATE   PROCEDURE fact.usp_PopulateDatabaseStatus
AS
BEGIN
    SET NOCOUNT ON;
    
    SELECT 
        cms.ServerName,
        COALESCE(cms.Environment, cms.CMSEnvironment, 'Unknown') AS Environment
    FROM config.ServerInventory cms
    WHERE cms.IsActive = 1
    AND (cms.ServerType = 'SQL Server' OR cms.ServerType IS NULL)
    ORDER BY cms.Environment, cms.ServerName;
END

GO

-- [FrameworkHealth].[usp_CleanupHistory]

CREATE PROCEDURE FrameworkHealth.usp_CleanupHistory
    @RetentionDays INT = 30
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @CutoffDate DATETIME2 = DATEADD(DAY, -@RetentionDays, SYSDATETIME());
    
    DELETE FROM FrameworkHealth.HealthCheckResults WHERE CheckTime < @CutoffDate;
    PRINT 'Deleted ' + CAST(@@ROWCOUNT AS VARCHAR(10)) + ' health check results.';
    
    DELETE FROM FrameworkHealth.HealthSummary WHERE CheckTime < @CutoffDate;
    PRINT 'Deleted ' + CAST(@@ROWCOUNT AS VARCHAR(10)) + ' health summaries.';
END

GO

-- [FrameworkHealth].[usp_RunHealthCheck]

CREATE PROCEDURE FrameworkHealth.usp_RunHealthCheck
    @SendNotification BIT = 1,
    @VerboseOutput BIT = 0
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @CheckTime DATETIME2(0) = SYSDATETIME();
    DECLARE @TotalChecks INT = 0;
    DECLARE @OKCount INT = 0;
    DECLARE @WarningCount INT = 0;
    DECLARE @CriticalCount INT = 0;
    DECLARE @ErrorCount INT = 0;
    DECLARE @JobsChecked INT = 0;
    DECLARE @JobsFailed INT = 0;
    DECLARE @ServersTotal INT = 0;
    DECLARE @ServersWithData INT = 0;
    
    PRINT '================================================================================';
    PRINT ' DBAOps Enterprise™ Framework Health Check';
    PRINT ' Server: ' + @@SERVERNAME;
    PRINT ' Check Time: ' + CONVERT(VARCHAR(30), @CheckTime, 120);
    PRINT '================================================================================';
    
    -- ========================================================================
    -- CHECK 1: KEY TABLES - Using dynamic SQL to handle column name differences
    -- ========================================================================
    PRINT '';
    PRINT '1. CHECKING KEY TABLES...';
    
    -- Check fact.BackupCompliance
    DECLARE @RowCount BIGINT = 0;
    DECLARE @LastDate DATETIME = NULL;
    DECLARE @SQL NVARCHAR(MAX);
    
    IF OBJECT_ID('fact.BackupCompliance', 'U') IS NOT NULL
    BEGIN
        -- Find the date column dynamically
        DECLARE @DateCol NVARCHAR(128);
        SELECT TOP 1 @DateCol = COLUMN_NAME 
        FROM INFORMATION_SCHEMA.COLUMNS 
        WHERE TABLE_SCHEMA = 'fact' AND TABLE_NAME = 'BackupCompliance'
          AND DATA_TYPE IN ('datetime', 'datetime2', 'date')
        ORDER BY ORDINAL_POSITION;
        
        IF @DateCol IS NOT NULL
        BEGIN
            SET @SQL = N'SELECT @cnt = COUNT(*), @dt = MAX(' + QUOTENAME(@DateCol) + N') FROM fact.BackupCompliance';
            EXEC sp_executesql @SQL, N'@cnt BIGINT OUTPUT, @dt DATETIME OUTPUT', @cnt = @RowCount OUTPUT, @dt = @LastDate OUTPUT;
        END
        ELSE
        BEGIN
            SELECT @RowCount = COUNT(*) FROM fact.BackupCompliance;
        END
        
        INSERT INTO FrameworkHealth.HealthCheckResults 
            (CheckTime, CheckCategory, ObjectName, Status, CurrentValue, Message, LastDataTime, [RowCount])
        VALUES (@CheckTime, 'Table', 'fact.BackupCompliance',
            CASE WHEN @LastDate > DATEADD(HOUR, -6, GETDATE()) THEN 'OK'
                 WHEN @LastDate > DATEADD(HOUR, -24, GETDATE()) THEN 'WARNING'
                 WHEN @LastDate IS NULL AND @RowCount > 0 THEN 'OK'
                 ELSE 'CRITICAL' END,
            CAST(@RowCount AS VARCHAR(20)) + ' rows',
            CASE WHEN @LastDate IS NOT NULL THEN 'Last data: ' + CONVERT(VARCHAR(30), @LastDate, 120) 
                 ELSE 'Table has ' + CAST(@RowCount AS VARCHAR(20)) + ' rows' END,
            @LastDate, @RowCount);
        
        SET @TotalChecks = @TotalChecks + 1;
        IF @LastDate > DATEADD(HOUR, -6, GETDATE()) OR (@LastDate IS NULL AND @RowCount > 0) 
            SET @OKCount = @OKCount + 1;
        ELSE IF @LastDate > DATEADD(HOUR, -24, GETDATE()) 
            SET @WarningCount = @WarningCount + 1;
        ELSE 
            SET @CriticalCount = @CriticalCount + 1;
        
        IF @VerboseOutput = 1
            PRINT '   fact.BackupCompliance: ' + CAST(@RowCount AS VARCHAR(20)) + ' rows';
    END
    
    -- Check fact.ServerHealth
    SET @RowCount = 0;
    SET @LastDate = NULL;
    
    IF OBJECT_ID('fact.ServerHealth', 'U') IS NOT NULL
    BEGIN
        SELECT TOP 1 @DateCol = COLUMN_NAME 
        FROM INFORMATION_SCHEMA.COLUMNS 
        WHERE TABLE_SCHEMA = 'fact' AND TABLE_NAME = 'ServerHealth'
          AND DATA_TYPE IN ('datetime', 'datetime2', 'date')
        ORDER BY ORDINAL_POSITION;
        
        IF @DateCol IS NOT NULL
        BEGIN
            SET @SQL = N'SELECT @cnt = COUNT(*), @dt = MAX(' + QUOTENAME(@DateCol) + N') FROM fact.ServerHealth';
            EXEC sp_executesql @SQL, N'@cnt BIGINT OUTPUT, @dt DATETIME OUTPUT', @cnt = @RowCount OUTPUT, @dt = @LastDate OUTPUT;
        END
        ELSE
        BEGIN
            SELECT @RowCount = COUNT(*) FROM fact.ServerHealth;
        END
        
        INSERT INTO FrameworkHealth.HealthCheckResults 
            (CheckTime, CheckCategory, ObjectName, Status, CurrentValue, Message, LastDataTime, [RowCount])
        VALUES (@CheckTime, 'Table', 'fact.ServerHealth',
            CASE WHEN @LastDate > DATEADD(HOUR, -2, GETDATE()) THEN 'OK'
                 WHEN @LastDate > DATEADD(HOUR, -6, GETDATE()) THEN 'WARNING'
                 WHEN @LastDate IS NULL AND @RowCount > 0 THEN 'OK'
                 ELSE 'CRITICAL' END,
            CAST(@RowCount AS VARCHAR(20)) + ' rows',
            CASE WHEN @LastDate IS NOT NULL THEN 'Last data: ' + CONVERT(VARCHAR(30), @LastDate, 120)
                 ELSE 'Table has ' + CAST(@RowCount AS VARCHAR(20)) + ' rows' END,
            @LastDate, @RowCount);
        
        SET @TotalChecks = @TotalChecks + 1;
        IF @LastDate > DATEADD(HOUR, -2, GETDATE()) OR (@LastDate IS NULL AND @RowCount > 0)
            SET @OKCount = @OKCount + 1;
        ELSE IF @LastDate > DATEADD(HOUR, -6, GETDATE()) 
            SET @WarningCount = @WarningCount + 1;
        ELSE 
            SET @CriticalCount = @CriticalCount + 1;
        
        IF @VerboseOutput = 1
            PRINT '   fact.ServerHealth: ' + CAST(@RowCount AS VARCHAR(20)) + ' rows';
    END
    
    -- Check config.ServerInventory
    SET @RowCount = 0;
    
    IF OBJECT_ID('config.ServerInventory', 'U') IS NOT NULL
    BEGIN
        SELECT @RowCount = COUNT(*) FROM config.ServerInventory;
        SET @ServersTotal = @RowCount;
        
        INSERT INTO FrameworkHealth.HealthCheckResults 
            (CheckTime, CheckCategory, ObjectName, Status, CurrentValue, Message, [RowCount])
        VALUES (@CheckTime, 'Table', 'config.ServerInventory',
            CASE WHEN @RowCount > 0 THEN 'OK' ELSE 'WARNING' END,
            CAST(@RowCount AS VARCHAR(20)) + ' servers',
            'Server inventory',
            @RowCount);
        
        SET @TotalChecks = @TotalChecks + 1;
        IF @RowCount > 0 SET @OKCount = @OKCount + 1;
        ELSE SET @WarningCount = @WarningCount + 1;
        
        IF @VerboseOutput = 1
            PRINT '   config.ServerInventory: ' + CAST(@RowCount AS VARCHAR(20)) + ' servers';
    END
    
    PRINT '   Tables checked: ' + CAST(@TotalChecks AS VARCHAR(10));
    
    -- ========================================================================
    -- CHECK 2: SQL AGENT JOBS
    -- ========================================================================
    PRINT '';
    PRINT '2. CHECKING SQL AGENT JOBS...';
    
    INSERT INTO FrameworkHealth.HealthCheckResults 
        (CheckTime, CheckCategory, ObjectName, Status, CurrentValue, Message)
    SELECT 
        @CheckTime,
        'Job',
        j.name,
        CASE 
            WHEN j.[enabled] = 0 THEN 'WARNING'
            WHEN h.run_status = 0 THEN 'CRITICAL'
            WHEN h.run_status = 1 THEN 'OK'
            WHEN h.run_status IS NULL THEN 'WARNING'
            ELSE 'OK'
        END,
        CASE 
            WHEN j.[enabled] = 0 THEN 'Disabled'
            WHEN h.run_status = 0 THEN 'Failed'
            WHEN h.run_status = 1 THEN 'Succeeded'
            WHEN h.run_status IS NULL THEN 'Never Run'
            ELSE 'Unknown'
        END,
        CASE 
            WHEN j.[enabled] = 0 THEN 'Job is disabled'
            WHEN h.run_status = 0 THEN 'Failed at ' + CONVERT(VARCHAR(30), msdb.dbo.agent_datetime(h.run_date, h.run_time), 120)
            WHEN h.run_status = 1 THEN 'OK'
            ELSE 'No recent execution'
        END
    FROM msdb.dbo.sysjobs j
    OUTER APPLY (
        SELECT TOP 1 run_status, run_date, run_time
        FROM msdb.dbo.sysjobhistory
        WHERE job_id = j.job_id AND step_id = 0
        ORDER BY run_date DESC, run_time DESC
    ) h
    WHERE j.name LIKE 'DBAOps%' 
       OR j.name LIKE 'Track-IT%';
    
    SET @JobsChecked = @@ROWCOUNT;
    
    SELECT @JobsFailed = COUNT(*) 
    FROM FrameworkHealth.HealthCheckResults 
    WHERE CheckTime = @CheckTime AND CheckCategory = 'Job' AND Status = 'CRITICAL';
    
    SELECT 
        @TotalChecks = @TotalChecks + COUNT(*),
        @OKCount = @OKCount + SUM(CASE WHEN Status = 'OK' THEN 1 ELSE 0 END),
        @WarningCount = @WarningCount + SUM(CASE WHEN Status = 'WARNING' THEN 1 ELSE 0 END),
        @CriticalCount = @CriticalCount + SUM(CASE WHEN Status = 'CRITICAL' THEN 1 ELSE 0 END)
    FROM FrameworkHealth.HealthCheckResults 
    WHERE CheckTime = @CheckTime AND CheckCategory = 'Job';
    
    PRINT '   Jobs checked: ' + CAST(@JobsChecked AS VARCHAR(10));
    PRINT '   Jobs failed: ' + CAST(@JobsFailed AS VARCHAR(10));
    
    -- ========================================================================
    -- CALCULATE HEALTH SCORE
    -- ========================================================================
    DECLARE @HealthScore DECIMAL(5,2);
    SET @HealthScore = CASE 
        WHEN @TotalChecks = 0 THEN 100.00
        ELSE CAST(@OKCount AS DECIMAL(10,2)) / CAST(@TotalChecks AS DECIMAL(10,2)) * 100
    END;
    
    -- Save summary
    INSERT INTO FrameworkHealth.HealthSummary (
        CheckTime, TotalChecks, OKCount, WarningCount, CriticalCount, ErrorCount,
        HealthScore, TablesHealthy, TablesStale, JobsHealthy, JobsFailed,
        ServersReachable, ServersUnreachable
    )
    VALUES (
        @CheckTime, @TotalChecks, @OKCount, @WarningCount, @CriticalCount, @ErrorCount,
        @HealthScore, @OKCount, @WarningCount + @CriticalCount, @JobsChecked - @JobsFailed, @JobsFailed,
        @ServersTotal, 0
    );
    
    -- ========================================================================
    -- PRINT SUMMARY
    -- ========================================================================
    PRINT '';
    PRINT '================================================================================';
    PRINT ' HEALTH CHECK SUMMARY';
    PRINT '================================================================================';
    PRINT '  Health Score: ' + CAST(@HealthScore AS VARCHAR(10)) + '%';
    PRINT '';
    PRINT '  Total Checks: ' + CAST(@TotalChecks AS VARCHAR(10));
    PRINT '  OK:           ' + CAST(@OKCount AS VARCHAR(10));
    PRINT '  Warnings:     ' + CAST(@WarningCount AS VARCHAR(10));
    PRINT '  Critical:     ' + CAST(@CriticalCount AS VARCHAR(10));
    PRINT '================================================================================';
    
    -- Send notification if needed
    IF @SendNotification = 1 AND (@CriticalCount > 0 OR @WarningCount > 5)
    BEGIN
        BEGIN TRY
            EXEC FrameworkHealth.usp_SendHealthNotification @CheckTime = @CheckTime;
        END TRY
        BEGIN CATCH
            PRINT 'Notification skipped: ' + ERROR_MESSAGE();
        END CATCH
        
        UPDATE FrameworkHealth.HealthSummary 
        SET NotificationSent = 1 
        WHERE CheckTime = @CheckTime;
    END
    
    -- Return summary
    SELECT 
        @CheckTime AS CheckTime,
        @HealthScore AS HealthScore,
        @TotalChecks AS TotalChecks,
        @OKCount AS OKCount,
        @WarningCount AS WarningCount,
        @CriticalCount AS CriticalCount;
END

GO

-- [FrameworkHealth].[usp_SelfHeal]

CREATE PROCEDURE FrameworkHealth.usp_SelfHeal
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @ActionTaken NVARCHAR(MAX) = '';
    
    -- Self-Healing Action 1: Restart failed jobs
    DECLARE @FailedJobs TABLE (JobName NVARCHAR(256));
    
    INSERT INTO @FailedJobs (JobName)
    SELECT j.name
    FROM msdb.dbo.sysjobs j
    INNER JOIN msdb.dbo.sysjobhistory h ON j.job_id = h.job_id
    WHERE j.name LIKE 'DBAOps%'
      AND h.step_id = 0
      AND h.run_status = 0
      AND msdb.dbo.agent_datetime(h.run_date, h.run_time) > DATEADD(HOUR, -1, GETDATE())
      AND j.enabled = 1;
    
    DECLARE @JobName NVARCHAR(256);
    DECLARE job_cursor CURSOR LOCAL FAST_FORWARD FOR SELECT JobName FROM @FailedJobs;
    OPEN job_cursor;
    FETCH NEXT FROM job_cursor INTO @JobName;
    
    WHILE @@FETCH_STATUS = 0
    BEGIN
        BEGIN TRY
            EXEC msdb.dbo.sp_start_job @job_name = @JobName;
            SET @ActionTaken = @ActionTaken + 'Restarted job: ' + @JobName + CHAR(13) + CHAR(10);
        END TRY
        BEGIN CATCH
            SET @ActionTaken = @ActionTaken + 'Failed to restart: ' + @JobName + ' - ' + ERROR_MESSAGE() + CHAR(13) + CHAR(10);
        END CATCH
        
        FETCH NEXT FROM job_cursor INTO @JobName;
    END
    
    CLOSE job_cursor;
    DEALLOCATE job_cursor;
    
    -- Self-Healing Action 2: Clear stale data
    DELETE FROM DBAOps.PerformanceTelemetry 
    WHERE CaptureTime < DATEADD(DAY, -7, GETDATE());
    
    IF @@ROWCOUNT > 0
        SET @ActionTaken = @ActionTaken + 'Cleaned ' + CAST(@@ROWCOUNT AS VARCHAR(10)) + ' stale performance records' + CHAR(13) + CHAR(10);
    
    -- Self-Healing Action 3: Update statistics on key tables
    DECLARE @TableName NVARCHAR(256);
    DECLARE @SQL NVARCHAR(MAX);
    
    DECLARE table_cursor CURSOR LOCAL FAST_FORWARD FOR
        SELECT QUOTENAME(s.name) + '.' + QUOTENAME(t.name)
        FROM sys.tables t
        JOIN sys.schemas s ON t.schema_id = s.schema_id
        WHERE s.name IN ('DBAOps', 'fact', 'security')
          AND t.name NOT LIKE '%History%';
    
    OPEN table_cursor;
    FETCH NEXT FROM table_cursor INTO @TableName;
    
    WHILE @@FETCH_STATUS = 0
    BEGIN
        SET @SQL = 'UPDATE STATISTICS ' + @TableName;
        EXEC sp_executesql @SQL;
        FETCH NEXT FROM table_cursor INTO @TableName;
    END
    
    CLOSE table_cursor;
    DEALLOCATE table_cursor;
    
    SET @ActionTaken = @ActionTaken + 'Updated statistics on key tables' + CHAR(13) + CHAR(10);
    
    -- Log self-healing actions
    IF LEN(@ActionTaken) > 0
    BEGIN
        INSERT INTO log.AutomationLog (LogType, LogMessage, LogTime)
        VALUES ('SelfHeal', @ActionTaken, GETDATE());
    END
END

GO

-- [FrameworkHealth].[usp_SendHealthNotification]

CREATE PROCEDURE FrameworkHealth.usp_SendHealthNotification
    @CheckTime DATETIME2 = NULL,
    @OnlyOnIssues BIT = 1  -- NEW: Only send if critical/warning
AS
BEGIN
    SET NOCOUNT ON;
    
    IF @CheckTime IS NULL
        SELECT TOP 1 @CheckTime = CheckTime FROM FrameworkHealth.HealthSummary ORDER BY CheckTime DESC;
    
    DECLARE @HealthScore DECIMAL(5,2);
    DECLARE @OKCount INT;
    DECLARE @WarningCount INT;
    DECLARE @CriticalCount INT;
    
    SELECT 
        @HealthScore = HealthScore,
        @OKCount = OKCount,
        @WarningCount = WarningCount,
        @CriticalCount = CriticalCount
    FROM FrameworkHealth.HealthSummary
    WHERE CheckTime = @CheckTime;
    
    -- =====================================================
    -- SMART EMAIL LOGIC: Only send if critical/warning
    -- =====================================================
    IF @OnlyOnIssues = 1 AND ISNULL(@CriticalCount, 0) = 0 AND ISNULL(@WarningCount, 0) <= 1
    BEGIN
        PRINT 'DBAOps | JB: Framework health score ' + CAST(ISNULL(@HealthScore, 0) AS VARCHAR(10)) + 
            '%. No critical issues. Email not sent.';
        RETURN;
    END;
    
    DECLARE @Subject NVARCHAR(500);
    SET @Subject = CASE 
        WHEN ISNULL(@CriticalCount, 0) > 0 THEN '[CRITICAL] '
        WHEN ISNULL(@WarningCount, 0) > 3 THEN '[WARNING] '
        ELSE '[OK] '
    END + 'DBAOps Enterprise™ Framework Health - ' + @@SERVERNAME + ' - Score: ' + CAST(ISNULL(@HealthScore, 0) AS VARCHAR(10)) + '%';
    
    DECLARE @Body NVARCHAR(MAX);
    SET @Body = N'<html><body style="font-family:Arial;">' +
        N'<h2>DBAOps Enterprise™ Framework Health Check</h2>' +
        N'<p><strong>Server:</strong> ' + @@SERVERNAME + N'</p>' +
        N'<p><strong>Time:</strong> ' + CONVERT(VARCHAR(30), @CheckTime, 120) + N'</p>' +
        N'<p><strong>Health Score:</strong> ' + CAST(ISNULL(@HealthScore, 0) AS VARCHAR(10)) + N'%</p>' +
        N'<p><strong>This report only sent when issues detected.</strong></p>' +
        N'<table border="1" cellpadding="5">' +
        N'<tr><td>Critical</td><td style="color:red;font-weight:bold;">' + CAST(ISNULL(@CriticalCount, 0) AS VARCHAR(10)) + N'</td></tr>' +
        N'<tr><td>Warnings</td><td style="color:orange;">' + CAST(ISNULL(@WarningCount, 0) AS VARCHAR(10)) + N'</td></tr>' +
        N'<tr><td>OK</td><td>' + CAST(ISNULL(@OKCount, 0) AS VARCHAR(10)) + N'</td></tr>' +
        N'</table></body></html>';
    
    BEGIN TRY
        EXEC msdb.dbo.sp_send_dbmail
            @recipients = N'SQLServerAdminNotification@YOURORG.gov',
            @subject = @Subject,
            @body = @Body,
            @body_format = N'HTML';
        PRINT 'DBAOps | JB: Framework health notification sent.';
    END TRY
    BEGIN CATCH
        PRINT 'Email failed: ' + ERROR_MESSAGE();
    END CATCH
END;

GO

-- [inventory].[usp_Collect_ApplicationMapping]

CREATE PROCEDURE inventory.usp_Collect_ApplicationMapping
/*
================================================================================
   DBAOps Monitoring and Compliance Framework
   Author: John Bosco RHOYLLE INNOVATION EMPIRE™, Sr SQL DBA/DBAOps Engineer
   
   Captures current connections to identify applications using databases
================================================================================
*/
    @ServerName NVARCHAR(128) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    SET @ServerName = ISNULL(@ServerName, @@SERVERNAME);
    
    INSERT INTO inventory.ApplicationMapping (
        ServerName, DatabaseName, ApplicationName, LoginName, HostName, ConnectionCount, LastSeen
    )
    SELECT 
        @ServerName,
        DB_NAME(database_id),
        ISNULL(program_name, 'Unknown'),
        ISNULL(login_name, 'Unknown'),
        ISNULL(host_name, 'Unknown'),
        COUNT(*),
        MAX(last_request_end_time)
    FROM sys.dm_exec_sessions
    WHERE database_id > 0
    AND session_id > 50
    GROUP BY database_id, program_name, login_name, host_name;
    
    PRINT 'Collected ' + CAST(@@ROWCOUNT AS VARCHAR) + ' application mappings';
END

GO

-- [inventory].[usp_Collect_DatabaseInventory_AllServers]

CREATE PROCEDURE inventory.usp_Collect_DatabaseInventory_AllServers
/*
================================================================================
   DBAOps Monitoring and Compliance Framework
   Author: John Bosco RHOYLLE INNOVATION EMPIRE™, Sr SQL DBA/DBAOps Engineer
   
   Collects database inventory from ALL servers in config.ServerReachability
================================================================================
*/
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @ServerName NVARCHAR(128);
    DECLARE @SQL NVARCHAR(MAX);
    DECLARE @Collected INT = 0;
    DECLARE @Errors INT = 0;
    DECLARE @TotalServers INT;
    
    -- Clear previous collection for today
    DELETE FROM inventory.DatabaseDetails 
    WHERE CAST(CollectedAt AS DATE) = CAST(GETDATE() AS DATE);
    
    SELECT @TotalServers = COUNT(DISTINCT ServerName) FROM config.ServerReachability;
    
    PRINT '================================================================================';
    PRINT '  Database Inventory Collection - All Servers';
    PRINT '  Started: ' + CONVERT(VARCHAR, GETDATE(), 120);
    PRINT '  Servers to process: ' + CAST(@TotalServers AS VARCHAR);
    PRINT '================================================================================';
    PRINT '';
    
    DECLARE server_cursor CURSOR FOR
    SELECT DISTINCT ServerName 
    FROM config.ServerReachability
    WHERE ServerName IS NOT NULL
    ORDER BY ServerName;
    
    OPEN server_cursor;
    FETCH NEXT FROM server_cursor INTO @ServerName;
    
    WHILE @@FETCH_STATUS = 0
    BEGIN
        BEGIN TRY
            SET @SQL = N'
            INSERT INTO inventory.DatabaseDetails (
                ServerName, DatabaseName, DatabaseID, CreateDate, CompatibilityLevel,
                RecoveryModel, StateDesc, CollationName, IsAutoCloseOn, IsAutoShrinkOn,
                IsReadOnly, PageVerifyOption, LogReuseWaitDesc, CollectedAt
            )
            SELECT 
                ''' + @ServerName + N''',
                name,
                database_id,
                create_date,
                compatibility_level,
                recovery_model_desc,
                state_desc,
                collation_name,
                is_auto_close_on,
                is_auto_shrink_on,
                is_read_only,
                page_verify_option_desc,
                log_reuse_wait_desc,
                GETDATE()
            FROM [' + @ServerName + N'].master.sys.databases
            WHERE database_id > 4
            AND state = 0';  -- Only ONLINE databases
            
            EXEC sp_executesql @SQL;
            SET @Collected = @Collected + 1;
            PRINT '  [OK] ' + @ServerName + ' (' + CAST(@Collected AS VARCHAR) + '/' + CAST(@TotalServers AS VARCHAR) + ')';
        END TRY
        BEGIN CATCH
            SET @Errors = @Errors + 1;
            PRINT '  [ERROR] ' + @ServerName + ': ' + LEFT(ERROR_MESSAGE(), 100);
        END CATCH
        
        FETCH NEXT FROM server_cursor INTO @ServerName;
    END
    
    CLOSE server_cursor;
    DEALLOCATE server_cursor;
    
    PRINT '';
    PRINT '================================================================================';
    PRINT '  Complete: ' + CAST(@Collected AS VARCHAR) + ' servers collected, ' + CAST(@Errors AS VARCHAR) + ' errors';
    PRINT '================================================================================';
END

GO

-- [inventory].[usp_Collect_FileGrowthConfig]

CREATE PROCEDURE inventory.usp_Collect_FileGrowthConfig
/*
================================================================================
   DBAOps Monitoring and Compliance Framework
   Author: John Bosco RHOYLLE INNOVATION EMPIRE™, Sr SQL DBA/DBAOps Engineer
   
   Collects file growth settings and flags non-compliant (percentage) growth
================================================================================
*/
    @ServerName NVARCHAR(128) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    SET @ServerName = ISNULL(@ServerName, @@SERVERNAME);
    
    INSERT INTO inventory.FileGrowthConfig (
        ServerName, DatabaseName, FileName, FileType, PhysicalPath,
        SizeMB, MaxSizeMB, GrowthValue, IsPercentGrowth, GrowthMB,
        GrowthDescription, IsCompliant, ComplianceNote
    )
    SELECT 
        @ServerName,
        DB_NAME(database_id),
        name,
        CASE type WHEN 0 THEN 'ROWS' WHEN 1 THEN 'LOG' END,
        physical_name,
        CAST(size * 8.0 / 1024 AS DECIMAL(18,2)),
        CASE max_size 
            WHEN -1 THEN -1 
            WHEN 268435456 THEN -1
            ELSE CAST(max_size * 8.0 / 1024 AS DECIMAL(18,2)) 
        END,
        growth,
        is_percent_growth,
        CASE 
            WHEN is_percent_growth = 1 THEN CAST(size * 8.0 / 1024 * growth / 100 AS DECIMAL(18,2))
            ELSE CAST(growth * 8.0 / 1024 AS DECIMAL(18,2))
        END,
        CASE 
            WHEN is_percent_growth = 1 THEN CAST(growth AS VARCHAR) + '%'
            ELSE CAST(CAST(growth * 8.0 / 1024 AS INT) AS VARCHAR) + ' MB'
        END,
        CASE WHEN is_percent_growth = 0 THEN 1 ELSE 0 END,
        CASE 
            WHEN is_percent_growth = 1 THEN 'WARNING: Percentage growth can cause performance issues'
            WHEN growth * 8 / 1024 < 64 THEN 'NOTE: Growth less than 64MB may cause frequent autogrowth'
            ELSE 'OK'
        END
    FROM sys.master_files
    WHERE database_id > 4;  -- Exclude system databases
    
    PRINT 'Collected file growth config for ' + CAST(@@ROWCOUNT AS VARCHAR) + ' files';
END

GO

-- [inventory].[usp_Collect_FileGrowthConfig_AllServers]

CREATE PROCEDURE inventory.usp_Collect_FileGrowthConfig_AllServers
/*
================================================================================
   DBAOps Monitoring and Compliance Framework
   Author: John Bosco RHOYLLE INNOVATION EMPIRE™, Sr SQL DBA/DBAOps Engineer
   
   Collects file growth configuration from ALL servers in config.ServerReachability
================================================================================
*/
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @ServerName NVARCHAR(128);
    DECLARE @SQL NVARCHAR(MAX);
    DECLARE @Collected INT = 0;
    DECLARE @Errors INT = 0;
    DECLARE @TotalServers INT;
    
    -- Clear previous collection for today
    DELETE FROM inventory.FileGrowthConfig 
    WHERE CAST(CollectedAt AS DATE) = CAST(GETDATE() AS DATE);
    
    SELECT @TotalServers = COUNT(DISTINCT ServerName) FROM config.ServerReachability;
    
    PRINT '================================================================================';
    PRINT '  File Growth Collection - All Servers';
    PRINT '  Started: ' + CONVERT(VARCHAR, GETDATE(), 120);
    PRINT '  Servers to process: ' + CAST(@TotalServers AS VARCHAR);
    PRINT '================================================================================';
    PRINT '';
    
    -- Use ALL servers from config.ServerReachability (no Reachable filter)
    DECLARE server_cursor CURSOR FOR
    SELECT DISTINCT ServerName 
    FROM config.ServerReachability
    WHERE ServerName IS NOT NULL
    ORDER BY ServerName;
    
    OPEN server_cursor;
    FETCH NEXT FROM server_cursor INTO @ServerName;
    
    WHILE @@FETCH_STATUS = 0
    BEGIN
        BEGIN TRY
            SET @SQL = N'
            INSERT INTO inventory.FileGrowthConfig (
                ServerName, DatabaseName, FileName, FileType, PhysicalPath,
                SizeMB, MaxSizeMB, GrowthValue, IsPercentGrowth, GrowthMB,
                GrowthDescription, IsCompliant, ComplianceNote, CollectedAt
            )
            SELECT 
                ''' + @ServerName + N''',
                DB_NAME(database_id),
                name,
                CASE type WHEN 0 THEN ''ROWS'' WHEN 1 THEN ''LOG'' END,
                physical_name,
                CAST(size * 8.0 / 1024 AS DECIMAL(18,2)),
                CASE max_size WHEN -1 THEN -1 WHEN 268435456 THEN -1
                    ELSE CAST(max_size * 8.0 / 1024 AS DECIMAL(18,2)) END,
                growth,
                is_percent_growth,
                CASE WHEN is_percent_growth = 1 THEN CAST(size * 8.0 / 1024 * growth / 100 AS DECIMAL(18,2))
                    ELSE CAST(growth * 8.0 / 1024 AS DECIMAL(18,2)) END,
                CASE WHEN is_percent_growth = 1 THEN CAST(growth AS VARCHAR) + ''%''
                    ELSE CAST(CAST(growth * 8.0 / 1024 AS INT) AS VARCHAR) + '' MB'' END,
                CASE WHEN is_percent_growth = 0 THEN 1 ELSE 0 END,
                CASE WHEN is_percent_growth = 1 THEN ''WARNING: Percentage growth''
                    WHEN growth * 8 / 1024 < 64 THEN ''NOTE: Growth < 64MB''
                    ELSE ''OK'' END,
                GETDATE()
            FROM [' + @ServerName + N'].master.sys.master_files
            WHERE database_id > 4';
            
            EXEC sp_executesql @SQL;
            SET @Collected = @Collected + 1;
            PRINT '  [OK] ' + @ServerName + ' (' + CAST(@Collected AS VARCHAR) + '/' + CAST(@TotalServers AS VARCHAR) + ')';
            
            -- Update reachability status
            UPDATE config.ServerReachability 
            SET Reachable = 1, CheckedOn = GETDATE()
            WHERE ServerName = @ServerName;
        END TRY
        BEGIN CATCH
            SET @Errors = @Errors + 1;
            PRINT '  [ERROR] ' + @ServerName + ': ' + LEFT(ERROR_MESSAGE(), 100);
            
            -- Update reachability status
            UPDATE config.ServerReachability 
            SET Reachable = 0, CheckedOn = GETDATE()
            WHERE ServerName = @ServerName;
        END CATCH
        
        FETCH NEXT FROM server_cursor INTO @ServerName;
    END
    
    CLOSE server_cursor;
    DEALLOCATE server_cursor;
    
    PRINT '';
    PRINT '================================================================================';
    PRINT '  Complete: ' + CAST(@Collected AS VARCHAR) + ' servers collected, ' + CAST(@Errors AS VARCHAR) + ' errors';
    PRINT '  Finished: ' + CONVERT(VARCHAR, GETDATE(), 120);
    PRINT '================================================================================';
END

GO

-- [inventory].[usp_CollectDatabaseInventory]

CREATE PROCEDURE [inventory].[usp_CollectDatabaseInventory]
    @ServerName NVARCHAR(256) = NULL,  -- NULL = all active servers
    @Debug BIT = 0
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @SQL NVARCHAR(MAX);
    DECLARE @CurrentServer NVARCHAR(256);
    DECLARE @LinkedServer NVARCHAR(256);
    DECLARE @ErrorMessage NVARCHAR(MAX);
    DECLARE @RowCount INT;
    
    -- Cursor through monitored servers
    DECLARE server_cursor CURSOR LOCAL FAST_FORWARD FOR
    SELECT ServerName, ISNULL(LinkedServerName, ServerName)
    FROM [inventory].[MonitoredServers]
    WHERE IsActive = 1
      AND (@ServerName IS NULL OR ServerName = @ServerName);
    
    OPEN server_cursor;
    FETCH NEXT FROM server_cursor INTO @CurrentServer, @LinkedServer;
    
    WHILE @@FETCH_STATUS = 0
    BEGIN
        BEGIN TRY
            IF @Debug = 1
                PRINT 'Processing server: ' + @CurrentServer;
            
            -- Update last checked timestamp
            UPDATE [inventory].[MonitoredServers]
            SET LastChecked = GETDATE()
            WHERE ServerName = @CurrentServer;
            
            -- Build dynamic SQL to collect database info from remote server
            -- For local server, use direct query; for remote, use linked server
            IF @LinkedServer = @@SERVERNAME OR @CurrentServer = @@SERVERNAME
            BEGIN
                -- Local server query
                INSERT INTO [inventory].[DatabaseInventorySnapshot]
                (ServerName, DatabaseName, DatabaseID, OwnerSID, OwnerName, 
                 CreateDate, StateDesc, RecoveryModelDesc, CompatibilityLevel, CollationName)
                SELECT 
                    @CurrentServer,
                    d.name,
                    d.database_id,
                    d.owner_sid,
                    SUSER_SNAME(d.owner_sid),
                    d.create_date,
                    d.state_desc,
                    d.recovery_model_desc,
                    d.compatibility_level,
                    d.collation_name
                FROM sys.databases d
                WHERE d.database_id > 4;  -- Exclude system databases
            END
            ELSE
            BEGIN
                -- Remote server query via linked server
                SET @SQL = N'
                INSERT INTO [inventory].[DatabaseInventorySnapshot]
                (ServerName, DatabaseName, DatabaseID, OwnerSID, OwnerName, 
                 CreateDate, StateDesc, RecoveryModelDesc, CompatibilityLevel, CollationName)
                SELECT 
                    @Server,
                    d.name,
                    d.database_id,
                    d.owner_sid,
                    SUSER_SNAME(d.owner_sid),
                    d.create_date,
                    d.state_desc,
                    d.recovery_model_desc,
                    d.compatibility_level,
                    d.collation_name
                FROM [' + @LinkedServer + '].master.sys.databases d
                WHERE d.database_id > 4;';
                
                EXEC sp_executesql @SQL, N'@Server NVARCHAR(256)', @Server = @CurrentServer;
            END
            
            SET @RowCount = @@ROWCOUNT;
            
            IF @Debug = 1
                PRINT 'Collected ' + CAST(@RowCount AS VARCHAR(10)) + ' databases from ' + @CurrentServer;
            
            -- Update success timestamp
            UPDATE [inventory].[MonitoredServers]
            SET LastSuccessful = GETDATE(),
                ErrorMessage = NULL
            WHERE ServerName = @CurrentServer;
            
        END TRY
        BEGIN CATCH
            SET @ErrorMessage = ERROR_MESSAGE();
            
            UPDATE [inventory].[MonitoredServers]
            SET ErrorMessage = @ErrorMessage
            WHERE ServerName = @CurrentServer;
            
            IF @Debug = 1
                PRINT 'Error collecting from ' + @CurrentServer + ': ' + @ErrorMessage;
        END CATCH
        
        FETCH NEXT FROM server_cursor INTO @CurrentServer, @LinkedServer;
    END
    
    CLOSE server_cursor;
    DEALLOCATE server_cursor;
END

GO

-- [inventory].[usp_CollectDatabaseInventory_AllServers_JB]

CREATE PROCEDURE [inventory].[usp_CollectDatabaseInventory_AllServers_JB]
/*
================================================================================
   DBAOps Monitoring and Compliance Framework
   Author: John Bosco RHOYLLE INNOVATION EMPIRE™ (JB), Sr SQL DBA/DBAOps Engineer
   
   Collects database inventory from ALL active servers in config.ServerInventory
   Uses linked server connections to gather database metadata
================================================================================
   COPYRIGHT (c) 2026 DBAOps Enterprise™ Framework | JB
================================================================================
*/
    @Debug BIT = 0
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @ServerName NVARCHAR(128);
    DECLARE @SQL NVARCHAR(MAX);
    DECLARE @RowCount INT;
    DECLARE @TotalServers INT = 0;
    DECLARE @SuccessCount INT = 0;
    DECLARE @FailCount INT = 0;
    DECLARE @ErrorMessage NVARCHAR(MAX);
    DECLARE @StartTime DATETIME = GETDATE();
    
    IF @Debug = 1
    BEGIN
        PRINT '================================================================================';
        PRINT '  DBAOps Database Inventory Collection - All Servers';
        PRINT '  Author: John Bosco RHOYLLE INNOVATION EMPIRE™ (JB)';
        PRINT '  Started: ' + CONVERT(VARCHAR, @StartTime, 120);
        PRINT '================================================================================';
    END
    
    -- Get servers from config.ServerInventory
    DECLARE server_cursor CURSOR LOCAL FAST_FORWARD FOR
    SELECT DISTINCT ServerName
    FROM [config].[ServerInventory]
    WHERE IsActive = 1
      AND ServerName IS NOT NULL
      AND ServerName <> ''
    ORDER BY ServerName;
    
    OPEN server_cursor;
    FETCH NEXT FROM server_cursor INTO @ServerName;
    
    WHILE @@FETCH_STATUS = 0
    BEGIN
        SET @TotalServers = @TotalServers + 1;
        
        BEGIN TRY
            -- Check if linked server exists
            IF EXISTS (SELECT 1 FROM sys.servers WHERE name = @ServerName)
            BEGIN
                SET @SQL = N'
                INSERT INTO [inventory].[DatabaseInventorySnapshot]
                (ServerName, DatabaseName, DatabaseID, OwnerName, CreateDate, 
                 StateDesc, RecoveryModelDesc, CompatibilityLevel, CollationName)
                SELECT 
                    @SrvName,
                    d.name,
                    d.database_id,
                    sp.name,
                    d.create_date,
                    d.state_desc,
                    d.recovery_model_desc,
                    d.compatibility_level,
                    d.collation_name
                FROM [' + @ServerName + N'].master.sys.databases d
                LEFT JOIN [' + @ServerName + N'].master.sys.server_principals sp 
                    ON d.owner_sid = sp.sid
                WHERE d.database_id > 4
                  AND d.state = 0
                  AND d.name NOT IN (''ReportServer'', ''ReportServerTempDB'', ''SSISDB'', ''distribution'');';
                
                EXEC sp_executesql @SQL, N'@SrvName NVARCHAR(128)', @SrvName = @ServerName;
                
                SET @RowCount = @@ROWCOUNT;
                SET @SuccessCount = @SuccessCount + 1;
                
                IF @Debug = 1
                    PRINT '  [OK] ' + @ServerName + ' - ' + CAST(@RowCount AS VARCHAR(10)) + ' databases';
            END
            ELSE
            BEGIN
                SET @FailCount = @FailCount + 1;
                IF @Debug = 1
                    PRINT '  [SKIP] ' + @ServerName + ' - No linked server';
            END
            
        END TRY
        BEGIN CATCH
            SET @ErrorMessage = ERROR_MESSAGE();
            SET @FailCount = @FailCount + 1;
            
            IF @Debug = 1
                PRINT '  [ERROR] ' + @ServerName + ': ' + LEFT(@ErrorMessage, 80);
        END CATCH
        
        FETCH NEXT FROM server_cursor INTO @ServerName;
    END
    
    CLOSE server_cursor;
    DEALLOCATE server_cursor;
    
    IF @Debug = 1
    BEGIN
        PRINT '';
        PRINT '================================================================================';
        PRINT '  Collection Summary | DBAOps | JB';
        PRINT '  Total Servers: ' + CAST(@TotalServers AS VARCHAR(10));
        PRINT '  Successful: ' + CAST(@SuccessCount AS VARCHAR(10));
        PRINT '  Failed/Skipped: ' + CAST(@FailCount AS VARCHAR(10));
        PRINT '  Duration: ' + CAST(DATEDIFF(SECOND, @StartTime, GETDATE()) AS VARCHAR(10)) + ' seconds';
        PRINT '================================================================================';
    END
    
    SELECT @TotalServers AS TotalServers, @SuccessCount AS Successful, @FailCount AS Failed;
END

GO

-- [inventory].[usp_CollectDiskSpace]

CREATE PROCEDURE inventory.usp_CollectDiskSpace
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @Inserted INT = 0;
    
    -- Insert into DBAOps.DiskSpaceHistory
    -- Columns: DiskSpaceID, ServerName, DriveLetter, DriveLabel, TotalSpaceGB,
    --          FreeSpaceGB, UsedSpaceGB, PercentFree, PercentUsed, CollectionTime
    
    INSERT INTO DBAOps.DiskSpaceHistory (
        ServerName, DriveLetter, DriveLabel, TotalSpaceGB, FreeSpaceGB,
        UsedSpaceGB, PercentFree, PercentUsed, CollectionTime
    )
    SELECT DISTINCT
        @@SERVERNAME AS ServerName,
        LEFT(vs.volume_mount_point, 1) AS DriveLetter,
        vs.logical_volume_name AS DriveLabel,
        CAST(vs.total_bytes / 1073741824.0 AS DECIMAL(18,2)) AS TotalSpaceGB,
        CAST(vs.available_bytes / 1073741824.0 AS DECIMAL(18,2)) AS FreeSpaceGB,
        CAST((vs.total_bytes - vs.available_bytes) / 1073741824.0 AS DECIMAL(18,2)) AS UsedSpaceGB,
        CAST(100.0 * vs.available_bytes / NULLIF(vs.total_bytes, 0) AS DECIMAL(5,2)) AS PercentFree,
        CAST(100.0 * (vs.total_bytes - vs.available_bytes) / NULLIF(vs.total_bytes, 0) AS DECIMAL(5,2)) AS PercentUsed,
        SYSDATETIME() AS CollectionTime
    FROM sys.master_files mf
    CROSS APPLY sys.dm_os_volume_stats(mf.database_id, mf.file_id) vs
    WHERE NOT EXISTS (
        SELECT 1 FROM DBAOps.DiskSpaceHistory dh
        WHERE dh.ServerName = @@SERVERNAME
          AND dh.DriveLetter = LEFT(vs.volume_mount_point, 1)
          AND dh.CollectionTime > DATEADD(HOUR, -1, SYSDATETIME())
    );
    
    SET @Inserted = @@ROWCOUNT;
    
    -- Log the operation
    INSERT INTO log.AutomationLog (ModuleName, RunStatus, Message, ServerName, RowsAffected, LogType, LogTime)
    VALUES ('CollectDiskSpace', 'Success', 'Collected ' + CAST(@Inserted AS VARCHAR(10)) + ' disk metrics',
            @@SERVERNAME, @Inserted, 'INFO', SYSDATETIME());
    
    PRINT 'Disk space collection completed: ' + CAST(@Inserted AS VARCHAR(10)) + ' drives';
    
    SELECT @Inserted AS DrivesCollected;
END;

GO

-- [inventory].[usp_DatabaseChangeDailySummary_JB]

CREATE PROCEDURE [inventory].[usp_DatabaseChangeDailySummary_JB]
/*
================================================================================
   DBAOps Monitoring and Compliance Framework
   Author: John Bosco RHOYLLE INNOVATION EMPIRE™ (JB), Sr SQL DBA/DBAOps Engineer
   
   Returns single-sentence summary for inclusion in daily reports
================================================================================
   COPYRIGHT (c) 2026 DBAOps Enterprise™ Framework | JB
================================================================================
*/
    @Hours INT = 24,
    @SummaryText NVARCHAR(MAX) = NULL OUTPUT
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @Added INT = 0, @Removed INT = 0, @Modified INT = 0, @Total INT = 0;
    DECLARE @Servers INT = 0;
    DECLARE @Summary NVARCHAR(MAX);
    DECLARE @LookbackDate DATETIME = DATEADD(HOUR, -@Hours, GETDATE());
    
    SELECT @Added = COUNT(*) FROM [inventory].[DatabaseChangeLog] WHERE ChangeType = 'DATABASE_ADDED' AND ChangeDetectedAt >= @LookbackDate;
    SELECT @Removed = COUNT(*) FROM [inventory].[DatabaseChangeLog] WHERE ChangeType = 'DATABASE_REMOVED' AND ChangeDetectedAt >= @LookbackDate;
    SELECT @Modified = COUNT(*) FROM [inventory].[DatabaseChangeLog] WHERE ChangeType NOT IN ('DATABASE_ADDED', 'DATABASE_REMOVED') AND ChangeDetectedAt >= @LookbackDate;
    SELECT @Servers = COUNT(DISTINCT ServerName) FROM [inventory].[DatabaseChangeLog] WHERE ChangeDetectedAt >= @LookbackDate;
    
    SET @Total = @Added + @Removed + @Modified;
    
    IF @Total = 0
        SET @Summary = N'âœ… Database Changes: No modifications detected across all monitored servers in the last ' + CAST(@Hours AS NVARCHAR(10)) + ' hours.';
    ELSE
    BEGIN
        SET @Summary = N'âš ï¸ Database Changes: ' + CAST(@Total AS NVARCHAR(10)) + ' change(s) on ' + CAST(@Servers AS NVARCHAR(10)) + ' server(s) â€” ';
        
        DECLARE @Parts NVARCHAR(500) = '';
        IF @Added > 0 SET @Parts = @Parts + CAST(@Added AS NVARCHAR(10)) + ' added, ';
        IF @Removed > 0 SET @Parts = @Parts + CAST(@Removed AS NVARCHAR(10)) + ' removed, ';
        IF @Modified > 0 SET @Parts = @Parts + CAST(@Modified AS NVARCHAR(10)) + ' modified, ';
        
        SET @Summary = @Summary + LEFT(@Parts, LEN(@Parts) - 1) + '.';
    END
    
    SET @SummaryText = @Summary;
    SELECT @Summary AS DailySummary;
END

GO

-- [inventory].[usp_DatabaseChangeTrackingJob]

CREATE PROCEDURE [inventory].[usp_DatabaseChangeTrackingJob]
    @Recipients NVARCHAR(MAX) = 'dba-team@yourcompany.com',
    @MailProfile NVARCHAR(256) = 'DBAMailProfile',
    @Debug BIT = 0
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @StartTime DATETIME = GETDATE();
    DECLARE @ChangesDetected INT = 0;
    
    BEGIN TRY
        IF @Debug = 1
            PRINT 'Starting Database Change Tracking Job at ' + CONVERT(VARCHAR, @StartTime, 120);
        
        -- Step 1: Collect current database inventory from all servers
        IF @Debug = 1
            PRINT 'Step 1: Collecting database inventory...';
        
        EXEC [inventory].[usp_CollectDatabaseInventory] @Debug = @Debug;
        
        -- Step 2: Detect changes by comparing to previous snapshot
        IF @Debug = 1
            PRINT 'Step 2: Detecting changes...';
        
        DECLARE @Changes TABLE (ChangesDetected INT);
        INSERT INTO @Changes
        EXEC [inventory].[usp_DetectDatabaseChanges] @Debug = @Debug;
        
        SELECT @ChangesDetected = ISNULL(ChangesDetected, 0) FROM @Changes;
        
        -- Step 3: Send notification if there are changes
        IF @ChangesDetected > 0
        BEGIN
            IF @Debug = 1
                PRINT 'Step 3: Sending notification for ' + CAST(@ChangesDetected AS VARCHAR(10)) + ' changes...';
            
            EXEC [inventory].[usp_SendDatabaseChangeNotification] 
                @Recipients = @Recipients,
                @MailProfile = @MailProfile,
                @SendOnlyIfChanges = 1,
                @Debug = @Debug;
        END
        ELSE
        BEGIN
            IF @Debug = 1
                PRINT 'No changes detected. Skipping notification.';
        END
        
        -- Step 4: Cleanup old snapshots (keep last 7 days)
        IF @Debug = 1
            PRINT 'Step 4: Cleaning up old snapshots...';
        
        DELETE FROM [inventory].[DatabaseInventorySnapshot]
        WHERE SnapshotDate < DATEADD(DAY, -7, GETDATE());
        
        IF @Debug = 1
        BEGIN
            PRINT 'Job completed successfully.';
            PRINT 'Duration: ' + CAST(DATEDIFF(SECOND, @StartTime, GETDATE()) AS VARCHAR(10)) + ' seconds';
        END
        
    END TRY
    BEGIN CATCH
        DECLARE @ErrorMessage NVARCHAR(MAX) = ERROR_MESSAGE();
        DECLARE @ErrorSeverity INT = ERROR_SEVERITY();
        DECLARE @ErrorState INT = ERROR_STATE();
        
        -- Log error (you may want to insert into an error log table)
        IF @Debug = 1
            PRINT 'Error in Database Change Tracking Job: ' + @ErrorMessage;
        
        -- Re-throw the error
        RAISERROR(@ErrorMessage, @ErrorSeverity, @ErrorState);
    END CATCH
END

GO

-- [inventory].[usp_DatabaseChangeTrackingJob_JB]

CREATE PROCEDURE [inventory].[usp_DatabaseChangeTrackingJob_JB]
/*
================================================================================
   DBAOps Monitoring and Compliance Framework
   Author: John Bosco RHOYLLE INNOVATION EMPIRE™ (JB), Sr SQL DBA/DBAOps Engineer
   
   Main orchestration procedure for database change tracking
   Called by SQL Server Agent job daily at 6:00 AM
================================================================================
   COPYRIGHT (c) 2026 DBAOps Enterprise™ Framework | JB
================================================================================
*/
    @Recipients NVARCHAR(MAX) = 'DBAOps@YOURORG.gov; SQLServerAdminNotification@YOURORG.gov',
    @MailProfile NVARCHAR(256) = 'DBAOpsProfile',
    @Debug BIT = 0
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @StartTime DATETIME = GETDATE();
    DECLARE @ChangesDetected INT = 0;
    
    BEGIN TRY
        IF @Debug = 1
        BEGIN
            PRINT '================================================================================';
            PRINT '  DBAOps Database Change Tracking Job';
            PRINT '  Author: John Bosco RHOYLLE INNOVATION EMPIRE™ (JB)';
            PRINT '  Started: ' + CONVERT(VARCHAR, @StartTime, 120);
            PRINT '================================================================================';
        END
        
        -- Step 1: Collect inventory from all servers
        IF @Debug = 1
            PRINT '[JB] Step 1: Collecting database inventory from all servers...';
        
        EXEC [inventory].[usp_CollectDatabaseInventory_AllServers_JB] @Debug = @Debug;
        
        -- Step 2: Detect changes
        IF @Debug = 1
            PRINT '[JB] Step 2: Detecting changes...';
        
        DECLARE @Changes TABLE (ChangesDetected INT);
        INSERT INTO @Changes
        EXEC [inventory].[usp_DetectDatabaseChanges_JB] @Debug = @Debug;
        
        SELECT @ChangesDetected = ISNULL(ChangesDetected, 0) FROM @Changes;
        
        -- Step 3: Send notification if changes detected
        IF @ChangesDetected > 0
        BEGIN
            IF @Debug = 1
                PRINT '[JB] Step 3: Sending notification for ' + CAST(@ChangesDetected AS VARCHAR(10)) + ' changes...';
            
            EXEC [inventory].[usp_SendDatabaseChangeNotification_JB]
                @Recipients = @Recipients,
                @MailProfile = @MailProfile,
                @SendOnlyIfChanges = 1,
                @Debug = @Debug;
        END
        ELSE
        BEGIN
            IF @Debug = 1
                PRINT '[JB] No changes detected. Skipping notification.';
        END
        
        -- Step 4: Cleanup old snapshots (keep 30 days)
        IF @Debug = 1
            PRINT '[JB] Step 4: Cleaning up old snapshots...';
        
        DELETE FROM [inventory].[DatabaseInventorySnapshot]
        WHERE SnapshotDate < DATEADD(DAY, -30, GETDATE());
        
        IF @Debug = 1
        BEGIN
            PRINT '';
            PRINT '================================================================================';
            PRINT '  Job completed successfully | DBAOps | JB';
            PRINT '  Duration: ' + CAST(DATEDIFF(SECOND, @StartTime, GETDATE()) AS VARCHAR(10)) + ' seconds';
            PRINT '================================================================================';
        END
        
    END TRY
    BEGIN CATCH
        DECLARE @ErrorMessage NVARCHAR(MAX) = ERROR_MESSAGE();
        
        IF @Debug = 1
            PRINT '[JB] ERROR: ' + @ErrorMessage;
        
        THROW;
    END CATCH
END

GO

-- [inventory].[usp_DetectDatabaseChanges]

CREATE PROCEDURE [inventory].[usp_DetectDatabaseChanges]
    @Debug BIT = 0
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @CurrentSnapshot DATETIME = GETDATE();
    DECLARE @PreviousSnapshotDate DATETIME;
    DECLARE @ChangesDetected INT = 0;
    
    -- Get the most recent complete snapshot date (before current collection)
    SELECT @PreviousSnapshotDate = MAX(SnapshotDate)
    FROM [inventory].[DatabaseInventorySnapshot]
    WHERE SnapshotDate < DATEADD(MINUTE, -5, @CurrentSnapshot);
    
    IF @PreviousSnapshotDate IS NULL
    BEGIN
        IF @Debug = 1
            PRINT 'No previous snapshot found. This appears to be the first run.';
        RETURN;
    END
    
    IF @Debug = 1
        PRINT 'Comparing current snapshot to previous from: ' + CONVERT(VARCHAR, @PreviousSnapshotDate, 120);
    
    -- Get current snapshot (within last 5 minutes)
    ;WITH CurrentSnapshot AS (
        SELECT ServerName, DatabaseName, DatabaseID, OwnerSID, OwnerName,
               CreateDate, StateDesc, RecoveryModelDesc, CompatibilityLevel
        FROM [inventory].[DatabaseInventorySnapshot]
        WHERE SnapshotDate >= DATEADD(MINUTE, -5, @CurrentSnapshot)
    ),
    PreviousSnapshot AS (
        SELECT ServerName, DatabaseName, DatabaseID, OwnerSID, OwnerName,
               CreateDate, StateDesc, RecoveryModelDesc, CompatibilityLevel
        FROM [inventory].[DatabaseInventorySnapshot]
        WHERE SnapshotDate >= DATEADD(MINUTE, -5, @PreviousSnapshotDate)
          AND SnapshotDate <= DATEADD(MINUTE, 5, @PreviousSnapshotDate)
    )
    
    -- Detect NEW databases (in current but not in previous)
    INSERT INTO [inventory].[DatabaseChangeLog]
    (ServerName, DatabaseName, ChangeType, NewValue, NewOwner, Creator, DatabaseCreateDate, Notes)
    SELECT 
        c.ServerName,
        c.DatabaseName,
        'DATABASE_ADDED',
        'New database detected',
        c.OwnerName,
        c.OwnerName,  -- Assuming owner is creator for new DBs
        c.CreateDate,
        'Database created on ' + CONVERT(VARCHAR, c.CreateDate, 120) + 
        ' | Recovery Model: ' + c.RecoveryModelDesc +
        ' | State: ' + c.StateDesc
    FROM CurrentSnapshot c
    LEFT JOIN PreviousSnapshot p ON c.ServerName = p.ServerName AND c.DatabaseName = p.DatabaseName
    WHERE p.DatabaseName IS NULL;
    
    SET @ChangesDetected = @ChangesDetected + @@ROWCOUNT;
    
    -- Detect REMOVED databases (in previous but not in current)
    ;WITH CurrentSnapshot AS (
        SELECT ServerName, DatabaseName
        FROM [inventory].[DatabaseInventorySnapshot]
        WHERE SnapshotDate >= DATEADD(MINUTE, -5, @CurrentSnapshot)
    ),
    PreviousSnapshot AS (
        SELECT ServerName, DatabaseName, OwnerName, CreateDate
        FROM [inventory].[DatabaseInventorySnapshot]
        WHERE SnapshotDate >= DATEADD(MINUTE, -5, @PreviousSnapshotDate)
          AND SnapshotDate <= DATEADD(MINUTE, 5, @PreviousSnapshotDate)
    )
    INSERT INTO [inventory].[DatabaseChangeLog]
    (ServerName, DatabaseName, ChangeType, OldValue, OldOwner, DatabaseCreateDate, Notes)
    SELECT 
        p.ServerName,
        p.DatabaseName,
        'DATABASE_REMOVED',
        'Database no longer exists',
        p.OwnerName,
        p.CreateDate,
        'Database was originally created on ' + ISNULL(CONVERT(VARCHAR, p.CreateDate, 120), 'Unknown')
    FROM PreviousSnapshot p
    LEFT JOIN CurrentSnapshot c ON p.ServerName = c.ServerName AND p.DatabaseName = c.DatabaseName
    WHERE c.DatabaseName IS NULL;
    
    SET @ChangesDetected = @ChangesDetected + @@ROWCOUNT;
    
    -- Detect OWNER changes
    ;WITH CurrentSnapshot AS (
        SELECT ServerName, DatabaseName, OwnerName, OwnerSID
        FROM [inventory].[DatabaseInventorySnapshot]
        WHERE SnapshotDate >= DATEADD(MINUTE, -5, @CurrentSnapshot)
    ),
    PreviousSnapshot AS (
        SELECT ServerName, DatabaseName, OwnerName, OwnerSID
        FROM [inventory].[DatabaseInventorySnapshot]
        WHERE SnapshotDate >= DATEADD(MINUTE, -5, @PreviousSnapshotDate)
          AND SnapshotDate <= DATEADD(MINUTE, 5, @PreviousSnapshotDate)
    )
    INSERT INTO [inventory].[DatabaseChangeLog]
    (ServerName, DatabaseName, ChangeType, OldValue, NewValue, OldOwner, NewOwner, Notes)
    SELECT 
        c.ServerName,
        c.DatabaseName,
        'OWNER_CHANGED',
        'Previous owner: ' + ISNULL(p.OwnerName, 'Unknown'),
        'New owner: ' + ISNULL(c.OwnerName, 'Unknown'),
        p.OwnerName,
        c.OwnerName,
        'Database ownership changed from ' + ISNULL(p.OwnerName, 'Unknown') + ' to ' + ISNULL(c.OwnerName, 'Unknown')
    FROM CurrentSnapshot c
    INNER JOIN PreviousSnapshot p ON c.ServerName = p.ServerName AND c.DatabaseName = p.DatabaseName
    WHERE ISNULL(CONVERT(NVARCHAR(200), c.OwnerSID, 1), '') <> ISNULL(CONVERT(NVARCHAR(200), p.OwnerSID, 1), '');
    
    SET @ChangesDetected = @ChangesDetected + @@ROWCOUNT;
    
    -- Detect STATE changes (Online/Offline/Restoring etc.)
    ;WITH CurrentSnapshot AS (
        SELECT ServerName, DatabaseName, StateDesc
        FROM [inventory].[DatabaseInventorySnapshot]
        WHERE SnapshotDate >= DATEADD(MINUTE, -5, @CurrentSnapshot)
    ),
    PreviousSnapshot AS (
        SELECT ServerName, DatabaseName, StateDesc
        FROM [inventory].[DatabaseInventorySnapshot]
        WHERE SnapshotDate >= DATEADD(MINUTE, -5, @PreviousSnapshotDate)
          AND SnapshotDate <= DATEADD(MINUTE, 5, @PreviousSnapshotDate)
    )
    INSERT INTO [inventory].[DatabaseChangeLog]
    (ServerName, DatabaseName, ChangeType, OldValue, NewValue, Notes)
    SELECT 
        c.ServerName,
        c.DatabaseName,
        'STATE_CHANGED',
        p.StateDesc,
        c.StateDesc,
        'Database state changed from ' + p.StateDesc + ' to ' + c.StateDesc
    FROM CurrentSnapshot c
    INNER JOIN PreviousSnapshot p ON c.ServerName = p.ServerName AND c.DatabaseName = p.DatabaseName
    WHERE c.StateDesc <> p.StateDesc;
    
    SET @ChangesDetected = @ChangesDetected + @@ROWCOUNT;
    
    -- Detect RECOVERY MODEL changes
    ;WITH CurrentSnapshot AS (
        SELECT ServerName, DatabaseName, RecoveryModelDesc
        FROM [inventory].[DatabaseInventorySnapshot]
        WHERE SnapshotDate >= DATEADD(MINUTE, -5, @CurrentSnapshot)
    ),
    PreviousSnapshot AS (
        SELECT ServerName, DatabaseName, RecoveryModelDesc
        FROM [inventory].[DatabaseInventorySnapshot]
        WHERE SnapshotDate >= DATEADD(MINUTE, -5, @PreviousSnapshotDate)
          AND SnapshotDate <= DATEADD(MINUTE, 5, @PreviousSnapshotDate)
    )
    INSERT INTO [inventory].[DatabaseChangeLog]
    (ServerName, DatabaseName, ChangeType, OldValue, NewValue, Notes)
    SELECT 
        c.ServerName,
        c.DatabaseName,
        'RECOVERY_MODEL_CHANGED',
        p.RecoveryModelDesc,
        c.RecoveryModelDesc,
        'Recovery model changed from ' + p.RecoveryModelDesc + ' to ' + c.RecoveryModelDesc
    FROM CurrentSnapshot c
    INNER JOIN PreviousSnapshot p ON c.ServerName = p.ServerName AND c.DatabaseName = p.DatabaseName
    WHERE c.RecoveryModelDesc <> p.RecoveryModelDesc;
    
    SET @ChangesDetected = @ChangesDetected + @@ROWCOUNT;
    
    IF @Debug = 1
        PRINT 'Total changes detected: ' + CAST(@ChangesDetected AS VARCHAR(10));
    
    -- Return the count of changes detected
    SELECT @ChangesDetected AS ChangesDetected;
END

GO

-- [inventory].[usp_DetectDatabaseChanges_JB]

CREATE PROCEDURE [inventory].[usp_DetectDatabaseChanges_JB]
/*
================================================================================
   DBAOps Monitoring and Compliance Framework
   Author: John Bosco RHOYLLE INNOVATION EMPIRE™ (JB), Sr SQL DBA/DBAOps Engineer
   
   Detects database changes by comparing current vs previous snapshots
   Tracks: Additions, Removals, Owner Changes, State Changes, Recovery Model Changes
================================================================================
   COPYRIGHT (c) 2026 DBAOps Enterprise™ Framework | JB
================================================================================
*/
    @Debug BIT = 0
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @CurrentSnapshotDate DATETIME;
    DECLARE @PreviousSnapshotDate DATETIME;
    DECLARE @ChangesDetected INT = 0;
    
    -- Get current snapshot date (most recent)
    SELECT @CurrentSnapshotDate = MAX(SnapshotDate)
    FROM [inventory].[DatabaseInventorySnapshot];
    
    -- Get previous snapshot date (day before current)
    SELECT @PreviousSnapshotDate = MAX(SnapshotDate)
    FROM [inventory].[DatabaseInventorySnapshot]
    WHERE CAST(SnapshotDate AS DATE) < CAST(@CurrentSnapshotDate AS DATE);
    
    IF @PreviousSnapshotDate IS NULL
    BEGIN
        IF @Debug = 1
            PRINT '[JB] No previous snapshot found. First run - baseline established.';
        SELECT 0 AS ChangesDetected;
        RETURN;
    END
    
    IF @Debug = 1
    BEGIN
        PRINT '[JB] Current Snapshot: ' + CONVERT(VARCHAR, @CurrentSnapshotDate, 120);
        PRINT '[JB] Previous Snapshot: ' + CONVERT(VARCHAR, @PreviousSnapshotDate, 120);
    END
    
    -- Detect NEW databases
    INSERT INTO [inventory].[DatabaseChangeLog]
    (ServerName, DatabaseName, ChangeType, NewValue, NewOwner, Creator, DatabaseCreateDate, Notes)
    SELECT 
        c.ServerName,
        c.DatabaseName,
        'DATABASE_ADDED',
        'New database detected',
        c.OwnerName,
        c.OwnerName,
        c.CreateDate,
        'Created: ' + ISNULL(CONVERT(VARCHAR, c.CreateDate, 120), 'Unknown') + 
        ' | Recovery: ' + ISNULL(c.RecoveryModelDesc, 'Unknown') +
        ' | State: ' + ISNULL(c.StateDesc, 'Unknown')
    FROM [inventory].[DatabaseInventorySnapshot] c
    LEFT JOIN [inventory].[DatabaseInventorySnapshot] p 
        ON c.ServerName = p.ServerName 
        AND c.DatabaseName = p.DatabaseName
        AND CAST(p.SnapshotDate AS DATE) = CAST(@PreviousSnapshotDate AS DATE)
    WHERE CAST(c.SnapshotDate AS DATE) = CAST(@CurrentSnapshotDate AS DATE)
      AND p.DatabaseName IS NULL;
    
    SET @ChangesDetected = @ChangesDetected + @@ROWCOUNT;
    
    -- Detect REMOVED databases
    INSERT INTO [inventory].[DatabaseChangeLog]
    (ServerName, DatabaseName, ChangeType, OldValue, OldOwner, DatabaseCreateDate, Notes)
    SELECT 
        p.ServerName,
        p.DatabaseName,
        'DATABASE_REMOVED',
        'Database no longer exists or offline',
        p.OwnerName,
        p.CreateDate,
        'Originally created: ' + ISNULL(CONVERT(VARCHAR, p.CreateDate, 120), 'Unknown')
    FROM [inventory].[DatabaseInventorySnapshot] p
    LEFT JOIN [inventory].[DatabaseInventorySnapshot] c 
        ON p.ServerName = c.ServerName 
        AND p.DatabaseName = c.DatabaseName
        AND CAST(c.SnapshotDate AS DATE) = CAST(@CurrentSnapshotDate AS DATE)
    WHERE CAST(p.SnapshotDate AS DATE) = CAST(@PreviousSnapshotDate AS DATE)
      AND c.DatabaseName IS NULL;
    
    SET @ChangesDetected = @ChangesDetected + @@ROWCOUNT;
    
    -- Detect OWNER changes
    INSERT INTO [inventory].[DatabaseChangeLog]
    (ServerName, DatabaseName, ChangeType, OldValue, NewValue, OldOwner, NewOwner, Notes)
    SELECT 
        c.ServerName,
        c.DatabaseName,
        'OWNER_CHANGED',
        'Previous: ' + ISNULL(p.OwnerName, 'Unknown'),
        'Current: ' + ISNULL(c.OwnerName, 'Unknown'),
        p.OwnerName,
        c.OwnerName,
        'Ownership changed from ' + ISNULL(p.OwnerName, 'Unknown') + ' to ' + ISNULL(c.OwnerName, 'Unknown')
    FROM [inventory].[DatabaseInventorySnapshot] c
    INNER JOIN [inventory].[DatabaseInventorySnapshot] p 
        ON c.ServerName = p.ServerName 
        AND c.DatabaseName = p.DatabaseName
    WHERE CAST(c.SnapshotDate AS DATE) = CAST(@CurrentSnapshotDate AS DATE)
      AND CAST(p.SnapshotDate AS DATE) = CAST(@PreviousSnapshotDate AS DATE)
      AND ISNULL(c.OwnerName, '') <> ISNULL(p.OwnerName, '');
    
    SET @ChangesDetected = @ChangesDetected + @@ROWCOUNT;
    
    -- Detect RECOVERY MODEL changes
    INSERT INTO [inventory].[DatabaseChangeLog]
    (ServerName, DatabaseName, ChangeType, OldValue, NewValue, Notes)
    SELECT 
        c.ServerName,
        c.DatabaseName,
        'RECOVERY_MODEL_CHANGED',
        p.RecoveryModelDesc,
        c.RecoveryModelDesc,
        'Recovery model changed from ' + ISNULL(p.RecoveryModelDesc, 'Unknown') + ' to ' + ISNULL(c.RecoveryModelDesc, 'Unknown')
    FROM [inventory].[DatabaseInventorySnapshot] c
    INNER JOIN [inventory].[DatabaseInventorySnapshot] p 
        ON c.ServerName = p.ServerName 
        AND c.DatabaseName = p.DatabaseName
    WHERE CAST(c.SnapshotDate AS DATE) = CAST(@CurrentSnapshotDate AS DATE)
      AND CAST(p.SnapshotDate AS DATE) = CAST(@PreviousSnapshotDate AS DATE)
      AND ISNULL(c.RecoveryModelDesc, '') <> ISNULL(p.RecoveryModelDesc, '');
    
    SET @ChangesDetected = @ChangesDetected + @@ROWCOUNT;
    
    IF @Debug = 1
        PRINT '[JB] Total changes detected: ' + CAST(@ChangesDetected AS VARCHAR(10));
    
    SELECT @ChangesDetected AS ChangesDetected;
END

GO

-- [inventory].[usp_Merge_LinkedServers]
-- Update the merge procedure to handle duplicates
CREATE   PROCEDURE [inventory].[usp_Merge_LinkedServers]
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @Updated INT, @Inserted INT;
    
    -- Update existing records (set Modified date)
    UPDATE A 
    SET Modified = GETDATE()
    FROM inventory.Linked_Servers A 
    INNER JOIN inventory.Linked_Servers_Load B 
        ON A.ServerName = B.ServerName 
        AND A.RmtServer = B.RmtServer 
        AND A.Data_Source = B.Data_Source
        AND A.Remote_Login = B.Remote_Login;
    
    SET @Updated = @@ROWCOUNT;
    
    -- Insert new records (with deduplication)
    ;WITH CTE AS (
        SELECT *,
            ROW_NUMBER() OVER (
                PARTITION BY ServerName, RmtServer, Data_Source, Remote_Login 
                ORDER BY ServerName
            ) AS rn
        FROM inventory.Linked_Servers_Load
    )
    INSERT INTO [inventory].[Linked_Servers]
        ([ServerName], [RmtServer], [SrvProduct], [provider], [Data_Source],
         [catalog], [rmtusername], [uses_self_credential], [Remote_Login], [Created])
    SELECT 
        A.[ServerName], A.[RmtServer], A.[SrvProduct], A.[provider], A.[Data_Source],
        A.[catalog], A.[rmtusername], A.[uses_self_credential], A.[Remote_Login],
        GETDATE() AS [Created]
    FROM CTE A
    LEFT JOIN inventory.Linked_Servers B 
        ON A.ServerName = B.ServerName 
        AND A.RmtServer = B.RmtServer 
        AND A.Data_Source = B.Data_Source
        AND A.Remote_Login = B.Remote_Login 
    WHERE A.rn = 1
      AND B.ServerName IS NULL;
    
    SET @Inserted = @@ROWCOUNT;
    
    PRINT 'Linked Servers Merge Complete - Updated: ' + CAST(@Updated AS VARCHAR(10)) + ', Inserted: ' + CAST(@Inserted AS VARCHAR(10));
END

GO

-- [inventory].[usp_SendDatabaseChangeNotification]

CREATE PROCEDURE [inventory].[usp_SendDatabaseChangeNotification]
    @Recipients NVARCHAR(MAX) = 'dba-team@yourcompany.com',  -- Comma-separated email list
    @MailProfile NVARCHAR(256) = 'DBAMailProfile',           -- Database Mail profile name
    @SendOnlyIfChanges BIT = 1,                               -- Only send if there are changes
    @Debug BIT = 0
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @HTMLBody NVARCHAR(MAX);
    DECLARE @Subject NVARCHAR(256);
    DECLARE @ChangeCount INT;
    DECLARE @ServerSummary NVARCHAR(MAX);
    
    -- Check for unsent notifications
    SELECT @ChangeCount = COUNT(*)
    FROM [inventory].[DatabaseChangeLog]
    WHERE NotificationSent = 0;
    
    IF @ChangeCount = 0 AND @SendOnlyIfChanges = 1
    BEGIN
        IF @Debug = 1
            PRINT 'No pending notifications to send.';
        RETURN;
    END
    
    -- Build server summary for subject line
    SELECT @ServerSummary = STRING_AGG(ServerName, ', ')
    FROM (SELECT DISTINCT TOP 5 ServerName FROM [inventory].[DatabaseChangeLog] WHERE NotificationSent = 0) AS servers;
    
    SET @Subject = N'âš ï¸ Database Change Alert: ' + CAST(@ChangeCount AS VARCHAR(10)) + 
                   ' change(s) detected on ' + ISNULL(@ServerSummary, 'SQL Servers');
    
    -- Build HTML Report
    SET @HTMLBody = N'
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <style>
        body {
            font-family: "Segoe UI", Arial, sans-serif;
            margin: 0;
            padding: 20px;
            background-color: #f5f5f5;
        }
        .container {
            max-width: 1200px;
            margin: 0 auto;
            background-color: #ffffff;
            border-radius: 8px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
            overflow: hidden;
        }
        .header {
            background: linear-gradient(135deg, #1e3a5f 0%, #2c5282 100%);
            color: white;
            padding: 25px 30px;
            text-align: center;
        }
        .header h1 {
            margin: 0 0 10px 0;
            font-size: 24px;
            font-weight: 600;
        }
        .header p {
            margin: 0;
            opacity: 0.9;
            font-size: 14px;
        }
        .summary-bar {
            display: flex;
            background-color: #f8f9fa;
            border-bottom: 1px solid #e9ecef;
            padding: 15px 30px;
        }
        .summary-item {
            flex: 1;
            text-align: center;
            padding: 10px;
        }
        .summary-item .number {
            font-size: 28px;
            font-weight: 700;
            color: #1e3a5f;
        }
        .summary-item .label {
            font-size: 12px;
            color: #6c757d;
            text-transform: uppercase;
            letter-spacing: 0.5px;
        }
        .summary-item.added .number { color: #28a745; }
        .summary-item.removed .number { color: #dc3545; }
        .summary-item.modified .number { color: #fd7e14; }
        .content {
            padding: 30px;
        }
        .section-title {
            font-size: 18px;
            font-weight: 600;
            color: #1e3a5f;
            margin: 25px 0 15px 0;
            padding-bottom: 10px;
            border-bottom: 2px solid #e9ecef;
        }
        .section-title:first-child {
            margin-top: 0;
        }
        table {
            width: 100%;
            border-collapse: collapse;
            margin-bottom: 20px;
            font-size: 13px;
        }
        th {
            background-color: #1e3a5f;
            color: white;
            padding: 12px 15px;
            text-align: left;
            font-weight: 600;
            text-transform: uppercase;
            font-size: 11px;
            letter-spacing: 0.5px;
        }
        td {
            padding: 12px 15px;
            border-bottom: 1px solid #e9ecef;
            vertical-align: top;
        }
        tr:hover {
            background-color: #f8f9fa;
        }
        .badge {
            display: inline-block;
            padding: 4px 10px;
            border-radius: 12px;
            font-size: 11px;
            font-weight: 600;
            text-transform: uppercase;
        }
        .badge-added {
            background-color: #d4edda;
            color: #155724;
        }
        .badge-removed {
            background-color: #f8d7da;
            color: #721c24;
        }
        .badge-owner {
            background-color: #fff3cd;
            color: #856404;
        }
        .badge-state {
            background-color: #cce5ff;
            color: #004085;
        }
        .badge-recovery {
            background-color: #e2e3e5;
            color: #383d41;
        }
        .server-name {
            font-weight: 600;
            color: #1e3a5f;
        }
        .db-name {
            font-weight: 500;
            color: #495057;
        }
        .timestamp {
            color: #6c757d;
            font-size: 12px;
        }
        .notes {
            font-size: 12px;
            color: #6c757d;
            max-width: 300px;
        }
        .footer {
            background-color: #f8f9fa;
            padding: 20px 30px;
            text-align: center;
            font-size: 12px;
            color: #6c757d;
            border-top: 1px solid #e9ecef;
        }
        .no-changes {
            text-align: center;
            padding: 40px;
            color: #6c757d;
        }
        .no-changes .icon {
            font-size: 48px;
            margin-bottom: 15px;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>ðŸ”” Database Change Notification Report</h1>
            <p>Generated: ' + CONVERT(VARCHAR, GETDATE(), 120) + ' | Repository: ' + @@SERVERNAME + '</p>
        </div>';
    
    -- Add summary counts
    DECLARE @AddedCount INT, @RemovedCount INT, @ModifiedCount INT;
    
    SELECT @AddedCount = COUNT(*) FROM [inventory].[DatabaseChangeLog] 
    WHERE NotificationSent = 0 AND ChangeType = 'DATABASE_ADDED';
    
    SELECT @RemovedCount = COUNT(*) FROM [inventory].[DatabaseChangeLog] 
    WHERE NotificationSent = 0 AND ChangeType = 'DATABASE_REMOVED';
    
    SELECT @ModifiedCount = COUNT(*) FROM [inventory].[DatabaseChangeLog] 
    WHERE NotificationSent = 0 AND ChangeType NOT IN ('DATABASE_ADDED', 'DATABASE_REMOVED');
    
    SET @HTMLBody = @HTMLBody + N'
        <div class="summary-bar">
            <div class="summary-item added">
                <div class="number">' + CAST(@AddedCount AS VARCHAR(10)) + '</div>
                <div class="label">Databases Added</div>
            </div>
            <div class="summary-item removed">
                <div class="number">' + CAST(@RemovedCount AS VARCHAR(10)) + '</div>
                <div class="label">Databases Removed</div>
            </div>
            <div class="summary-item modified">
                <div class="number">' + CAST(@ModifiedCount AS VARCHAR(10)) + '</div>
                <div class="label">Configurations Changed</div>
            </div>
            <div class="summary-item">
                <div class="number">' + CAST(@ChangeCount AS VARCHAR(10)) + '</div>
                <div class="label">Total Changes</div>
            </div>
        </div>
        <div class="content">';
    
    -- Section: Newly Added Databases
    IF @AddedCount > 0
    BEGIN
        SET @HTMLBody = @HTMLBody + N'
            <div class="section-title">ðŸ“¥ Newly Added Databases</div>
            <table>
                <tr>
                    <th>Server</th>
                    <th>Database</th>
                    <th>Owner/Creator</th>
                    <th>Created Date</th>
                    <th>Detected At</th>
                    <th>Details</th>
                </tr>';
        
        SELECT @HTMLBody = @HTMLBody + N'
                <tr>
                    <td class="server-name">' + ISNULL(ServerName, '') + '</td>
                    <td class="db-name">' + ISNULL(DatabaseName, '') + '</td>
                    <td>' + ISNULL(NewOwner, ISNULL(Creator, 'Unknown')) + '</td>
                    <td class="timestamp">' + ISNULL(CONVERT(VARCHAR, DatabaseCreateDate, 120), 'N/A') + '</td>
                    <td class="timestamp">' + CONVERT(VARCHAR, ChangeDetectedAt, 120) + '</td>
                    <td class="notes">' + ISNULL(Notes, '') + '</td>
                </tr>'
        FROM [inventory].[DatabaseChangeLog]
        WHERE NotificationSent = 0 AND ChangeType = 'DATABASE_ADDED'
        ORDER BY ChangeDetectedAt DESC;
        
        SET @HTMLBody = @HTMLBody + N'
            </table>';
    END
    
    -- Section: Removed Databases
    IF @RemovedCount > 0
    BEGIN
        SET @HTMLBody = @HTMLBody + N'
            <div class="section-title">ðŸ“¤ Removed Databases</div>
            <table>
                <tr>
                    <th>Server</th>
                    <th>Database</th>
                    <th>Previous Owner</th>
                    <th>Original Create Date</th>
                    <th>Removal Detected</th>
                    <th>Notes</th>
                </tr>';
        
        SELECT @HTMLBody = @HTMLBody + N'
                <tr>
                    <td class="server-name">' + ISNULL(ServerName, '') + '</td>
                    <td class="db-name">' + ISNULL(DatabaseName, '') + '</td>
                    <td>' + ISNULL(OldOwner, 'Unknown') + '</td>
                    <td class="timestamp">' + ISNULL(CONVERT(VARCHAR, DatabaseCreateDate, 120), 'N/A') + '</td>
                    <td class="timestamp">' + CONVERT(VARCHAR, ChangeDetectedAt, 120) + '</td>
                    <td class="notes">' + ISNULL(Notes, '') + '</td>
                </tr>'
        FROM [inventory].[DatabaseChangeLog]
        WHERE NotificationSent = 0 AND ChangeType = 'DATABASE_REMOVED'
        ORDER BY ChangeDetectedAt DESC;
        
        SET @HTMLBody = @HTMLBody + N'
            </table>';
    END
    
    -- Section: Configuration Changes
    IF @ModifiedCount > 0
    BEGIN
        SET @HTMLBody = @HTMLBody + N'
            <div class="section-title">âš™ï¸ Configuration Changes</div>
            <table>
                <tr>
                    <th>Server</th>
                    <th>Database</th>
                    <th>Change Type</th>
                    <th>Previous Value</th>
                    <th>New Value</th>
                    <th>Detected At</th>
                </tr>';
        
        SELECT @HTMLBody = @HTMLBody + N'
                <tr>
                    <td class="server-name">' + ISNULL(ServerName, '') + '</td>
                    <td class="db-name">' + ISNULL(DatabaseName, '') + '</td>
                    <td><span class="badge ' + 
                        CASE 
                            WHEN ChangeType = 'OWNER_CHANGED' THEN 'badge-owner'
                            WHEN ChangeType = 'STATE_CHANGED' THEN 'badge-state'
                            WHEN ChangeType = 'RECOVERY_MODEL_CHANGED' THEN 'badge-recovery'
                            ELSE 'badge-state'
                        END + '">' + 
                        REPLACE(ChangeType, '_', ' ') + '</span></td>
                    <td>' + ISNULL(OldValue, ISNULL(OldOwner, 'N/A')) + '</td>
                    <td>' + ISNULL(NewValue, ISNULL(NewOwner, 'N/A')) + '</td>
                    <td class="timestamp">' + CONVERT(VARCHAR, ChangeDetectedAt, 120) + '</td>
                </tr>'
        FROM [inventory].[DatabaseChangeLog]
        WHERE NotificationSent = 0 AND ChangeType NOT IN ('DATABASE_ADDED', 'DATABASE_REMOVED')
        ORDER BY ChangeDetectedAt DESC;
        
        SET @HTMLBody = @HTMLBody + N'
            </table>';
    END
    
    -- Close HTML
    SET @HTMLBody = @HTMLBody + N'
        </div>
        <div class="footer">
            <p>This is an automated notification from the DBAOps Repository on ' + @@SERVERNAME + '</p>
            <p>Generated by: [inventory].[usp_SendDatabaseChangeNotification]</p>
        </div>
    </div>
</body>
</html>';
    
    -- Send email
    IF @ChangeCount > 0 OR @SendOnlyIfChanges = 0
    BEGIN
        BEGIN TRY
            EXEC msdb.dbo.sp_send_dbmail
                @profile_name = @MailProfile,
                @recipients = @Recipients,
                @subject = @Subject,
                @body = @HTMLBody,
                @body_format = 'HTML';
            
            -- Mark notifications as sent
            UPDATE [inventory].[DatabaseChangeLog]
            SET NotificationSent = 1,
                NotificationSentAt = GETDATE()
            WHERE NotificationSent = 0;
            
            IF @Debug = 1
                PRINT 'Email sent successfully to: ' + @Recipients;
                
        END TRY
        BEGIN CATCH
            IF @Debug = 1
                PRINT 'Error sending email: ' + ERROR_MESSAGE();
            
            THROW;
        END CATCH
    END
END

GO

-- [inventory].[usp_SendDatabaseChangeNotification_JB]

CREATE PROCEDURE [inventory].[usp_SendDatabaseChangeNotification_JB]
/*
================================================================================
   DBAOps Monitoring and Compliance Framework
   Author: John Bosco RHOYLLE INNOVATION EMPIRE™ (JB), Sr SQL DBA/DBAOps Engineer
   
   Generates HTML report and sends email notification for database changes
================================================================================
   COPYRIGHT (c) 2026 DBAOps Enterprise™ Framework | JB
================================================================================
*/
    @Recipients NVARCHAR(MAX) = 'DBAOps@YOURORG.gov; SQLServerAdminNotification@YOURORG.gov',
    @MailProfile NVARCHAR(256) = 'DBAOpsProfile',
    @SendOnlyIfChanges BIT = 1,
    @Debug BIT = 0
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @HTMLBody NVARCHAR(MAX);
    DECLARE @Subject NVARCHAR(256);
    DECLARE @ChangeCount INT;
    DECLARE @AddedCount INT = 0, @RemovedCount INT = 0, @ModifiedCount INT = 0;
    DECLARE @ServerSummary NVARCHAR(500);
    
    -- Get counts
    SELECT @ChangeCount = COUNT(*) FROM [inventory].[DatabaseChangeLog] WHERE NotificationSent = 0;
    SELECT @AddedCount = COUNT(*) FROM [inventory].[DatabaseChangeLog] WHERE NotificationSent = 0 AND ChangeType = 'DATABASE_ADDED';
    SELECT @RemovedCount = COUNT(*) FROM [inventory].[DatabaseChangeLog] WHERE NotificationSent = 0 AND ChangeType = 'DATABASE_REMOVED';
    SELECT @ModifiedCount = COUNT(*) FROM [inventory].[DatabaseChangeLog] WHERE NotificationSent = 0 AND ChangeType NOT IN ('DATABASE_ADDED', 'DATABASE_REMOVED');
    
    IF @ChangeCount = 0 AND @SendOnlyIfChanges = 1
    BEGIN
        IF @Debug = 1
            PRINT '[JB] No pending notifications to send.';
        RETURN;
    END
    
    -- Get affected servers for subject
    SELECT @ServerSummary = STRING_AGG(ServerName, ', ') WITHIN GROUP (ORDER BY ServerName)
    FROM (SELECT DISTINCT TOP 5 ServerName FROM [inventory].[DatabaseChangeLog] WHERE NotificationSent = 0) s;
    
    SET @Subject = N'âš ï¸ DBAOps Database Changes: ' + CAST(@ChangeCount AS VARCHAR(10)) + 
                   ' change(s) on ' + ISNULL(@ServerSummary, 'SQL Servers');
    
    -- Build HTML
    SET @HTMLBody = N'
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Segoe UI, Arial, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
.container { max-width: 1100px; margin: 0 auto; background: #fff; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); overflow: hidden; }
.header { background: linear-gradient(135deg, #1a5276 0%, #2980b9 100%); color: white; padding: 25px; text-align: center; }
.header h1 { margin: 0 0 5px 0; font-size: 22px; }
.header p { margin: 0; opacity: 0.9; font-size: 13px; }
.summary { display: flex; background: #f8f9fa; border-bottom: 1px solid #e9ecef; }
.summary-item { flex: 1; text-align: center; padding: 20px; }
.summary-item .num { font-size: 32px; font-weight: 700; }
.summary-item .label { font-size: 11px; color: #6c757d; text-transform: uppercase; }
.added .num { color: #28a745; }
.removed .num { color: #dc3545; }
.modified .num { color: #fd7e14; }
.content { padding: 25px; }
.section { margin-bottom: 25px; }
.section-title { font-size: 16px; font-weight: 600; color: #1a5276; margin-bottom: 12px; padding-bottom: 8px; border-bottom: 2px solid #e9ecef; }
table { width: 100%; border-collapse: collapse; font-size: 12px; }
th { background: #1a5276; color: white; padding: 10px 12px; text-align: left; font-weight: 600; }
td { padding: 10px 12px; border-bottom: 1px solid #e9ecef; }
tr:hover { background: #f8f9fa; }
.server { font-weight: 600; color: #1a5276; }
.db { color: #495057; }
.timestamp { color: #6c757d; font-size: 11px; }
.footer { background: #f8f9fa; padding: 15px; text-align: center; font-size: 11px; color: #6c757d; border-top: 1px solid #e9ecef; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>ðŸ”” Database Change Notification</h1>
<p>Generated: ' + CONVERT(VARCHAR, GETDATE(), 120) + ' | DBAOps Enterprise™ Framework | JB</p>
</div>
<div class="summary">
<div class="summary-item added"><div class="num">' + CAST(@AddedCount AS VARCHAR) + '</div><div class="label">Added</div></div>
<div class="summary-item removed"><div class="num">' + CAST(@RemovedCount AS VARCHAR) + '</div><div class="label">Removed</div></div>
<div class="summary-item modified"><div class="num">' + CAST(@ModifiedCount AS VARCHAR) + '</div><div class="label">Modified</div></div>
<div class="summary-item"><div class="num">' + CAST(@ChangeCount AS VARCHAR) + '</div><div class="label">Total</div></div>
</div>
<div class="content">';

    -- Added databases section
    IF @AddedCount > 0
    BEGIN
        SET @HTMLBody = @HTMLBody + N'
<div class="section">
<div class="section-title">ðŸ“¥ Newly Added Databases (' + CAST(@AddedCount AS VARCHAR) + ')</div>
<table>
<tr><th>Server</th><th>Database</th><th>Owner/Creator</th><th>Created</th><th>Detected</th><th>Details</th></tr>';
        
        SELECT @HTMLBody = @HTMLBody + N'
<tr>
<td class="server">' + ISNULL(ServerName, '') + '</td>
<td class="db">' + ISNULL(DatabaseName, '') + '</td>
<td>' + ISNULL(NewOwner, ISNULL(Creator, 'Unknown')) + '</td>
<td class="timestamp">' + ISNULL(CONVERT(VARCHAR, DatabaseCreateDate, 120), 'N/A') + '</td>
<td class="timestamp">' + CONVERT(VARCHAR, ChangeDetectedAt, 120) + '</td>
<td>' + ISNULL(LEFT(Notes, 100), '') + '</td>
</tr>'
        FROM [inventory].[DatabaseChangeLog]
        WHERE NotificationSent = 0 AND ChangeType = 'DATABASE_ADDED'
        ORDER BY ChangeDetectedAt DESC;
        
        SET @HTMLBody = @HTMLBody + N'</table></div>';
    END

    -- Removed databases section
    IF @RemovedCount > 0
    BEGIN
        SET @HTMLBody = @HTMLBody + N'
<div class="section">
<div class="section-title">ðŸ“¤ Removed Databases (' + CAST(@RemovedCount AS VARCHAR) + ')</div>
<table>
<tr><th>Server</th><th>Database</th><th>Previous Owner</th><th>Original Create Date</th><th>Removal Detected</th></tr>';
        
        SELECT @HTMLBody = @HTMLBody + N'
<tr>
<td class="server">' + ISNULL(ServerName, '') + '</td>
<td class="db">' + ISNULL(DatabaseName, '') + '</td>
<td>' + ISNULL(OldOwner, 'Unknown') + '</td>
<td class="timestamp">' + ISNULL(CONVERT(VARCHAR, DatabaseCreateDate, 120), 'N/A') + '</td>
<td class="timestamp">' + CONVERT(VARCHAR, ChangeDetectedAt, 120) + '</td>
</tr>'
        FROM [inventory].[DatabaseChangeLog]
        WHERE NotificationSent = 0 AND ChangeType = 'DATABASE_REMOVED'
        ORDER BY ChangeDetectedAt DESC;
        
        SET @HTMLBody = @HTMLBody + N'</table></div>';
    END

    -- Configuration changes section
    IF @ModifiedCount > 0
    BEGIN
        SET @HTMLBody = @HTMLBody + N'
<div class="section">
<div class="section-title">âš™ï¸ Configuration Changes (' + CAST(@ModifiedCount AS VARCHAR) + ')</div>
<table>
<tr><th>Server</th><th>Database</th><th>Change Type</th><th>Previous</th><th>Current</th><th>Detected</th></tr>';
        
        SELECT @HTMLBody = @HTMLBody + N'
<tr>
<td class="server">' + ISNULL(ServerName, '') + '</td>
<td class="db">' + ISNULL(DatabaseName, '') + '</td>
<td>' + REPLACE(ChangeType, '_', ' ') + '</td>
<td>' + ISNULL(OldValue, ISNULL(OldOwner, 'N/A')) + '</td>
<td>' + ISNULL(NewValue, ISNULL(NewOwner, 'N/A')) + '</td>
<td class="timestamp">' + CONVERT(VARCHAR, ChangeDetectedAt, 120) + '</td>
</tr>'
        FROM [inventory].[DatabaseChangeLog]
        WHERE NotificationSent = 0 AND ChangeType NOT IN ('DATABASE_ADDED', 'DATABASE_REMOVED')
        ORDER BY ChangeDetectedAt DESC;
        
        SET @HTMLBody = @HTMLBody + N'</table></div>';
    END

    -- Close HTML
    SET @HTMLBody = @HTMLBody + N'
</div>
<div class="footer">
DBAOps Monitoring Framework | YOURSERVER01 | Author: John Bosco RHOYLLE INNOVATION EMPIRE™ (JB)<br/>
Â© 2026 Your Organization - All Rights Reserved
</div>
</div>
</body>
</html>';

    -- Send email
    BEGIN TRY
        EXEC msdb.dbo.sp_send_dbmail
            @profile_name = @MailProfile,
            @recipients = @Recipients,
            @subject = @Subject,
            @body = @HTMLBody,
            @body_format = 'HTML';
        
        -- Mark as sent
        UPDATE [inventory].[DatabaseChangeLog]
        SET NotificationSent = 1, NotificationSentAt = GETDATE()
        WHERE NotificationSent = 0;
        
        IF @Debug = 1
            PRINT '[JB] Email sent successfully to: ' + @Recipients;
    END TRY
    BEGIN CATCH
        IF @Debug = 1
            PRINT '[JB] Error sending email: ' + ERROR_MESSAGE();
        THROW;
    END CATCH
END

GO

-- [inventory].[usp_ViewRecentDatabaseChanges]

CREATE PROCEDURE [inventory].[usp_ViewRecentDatabaseChanges]
    @Days INT = 7,
    @ServerName NVARCHAR(256) = NULL,
    @ChangeType NVARCHAR(50) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    SELECT 
        ChangeLogID,
        ServerName,
        DatabaseName,
        ChangeType,
        OldValue,
        NewValue,
        COALESCE(OldOwner, '') AS OldOwner,
        COALESCE(NewOwner, Creator, '') AS NewOwner,
        ChangeDetectedAt,
        DatabaseCreateDate,
        CASE WHEN NotificationSent = 1 THEN 'Yes' ELSE 'No' END AS NotificationSent,
        NotificationSentAt,
        Notes
    FROM [inventory].[DatabaseChangeLog]
    WHERE ChangeDetectedAt >= DATEADD(DAY, -@Days, GETDATE())
      AND (@ServerName IS NULL OR ServerName = @ServerName)
      AND (@ChangeType IS NULL OR ChangeType = @ChangeType)
    ORDER BY ChangeDetectedAt DESC;
END

GO

-- [log].[usp_DBAOps_SendNotification]

CREATE   PROCEDURE log.usp_DBAOps_SendNotification
(
    @ModuleName      VARCHAR(200),
    @StatusText      VARCHAR(50),
    @DetailedMessage VARCHAR(MAX),
    @ActionRequired  VARCHAR(MAX),
    @JobName         VARCHAR(200) = NULL,
    @StepName        VARCHAR(200) = NULL,
    @ErrorMessage    VARCHAR(MAX) = NULL,
    @StartTime       DATETIME = NULL,
    @EndTime         DATETIME = NULL,
    @ProfileName     VARCHAR(200) = 'DBAOpsProfile'
)
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @Duration INT = DATEDIFF(SECOND, @StartTime, @EndTime);
    DECLARE @ServerName VARCHAR(200) = @@SERVERNAME;

    ----------------------------------------------
    -- 1. Log to table
    ----------------------------------------------
    INSERT INTO log.NotificationLog
    (
        ModuleName, JobName, StepName, StatusText,
        DetailedMessage, ActionRequired, ErrorMessage,
        SQLServerName, StartTime, EndTime, DurationSeconds
    )
    VALUES
    (
        @ModuleName, @JobName, @StepName, @StatusText,
        @DetailedMessage, @ActionRequired, @ErrorMessage,
        @ServerName, @StartTime, @EndTime, @Duration
    );

    ----------------------------------------------
    -- 2. Build subject
    ----------------------------------------------
    DECLARE @Subject NVARCHAR(300) =
        '*** ' + @ModuleName + ' â€“ ' + UPPER(@StatusText) + ' ***';

    ----------------------------------------------
    -- 3. Build HTML body
    ----------------------------------------------
    DECLARE @Body NVARCHAR(MAX) = '
    <html>
    <body style="font-family:Calibri; font-size:14px;">
        <h2 style="color:#004b8d;">DBAOps Notification</h2>

        <table style="border-collapse:collapse;">
            <tr><td><b>Module:</b></td><td>' + @ModuleName + '</td></tr>
            <tr><td><b>Status:</b></td><td>' + @StatusText + '</td></tr>
            <tr><td><b>Server:</b></td><td>' + @ServerName + '</td></tr>
            <tr><td><b>Job:</b></td><td>' + ISNULL(@JobName,'N/A') + '</td></tr>
            <tr><td><b>Step:</b></td><td>' + ISNULL(@StepName,'N/A') + '</td></tr>
            <tr><td><b>Start Time:</b></td><td>' + CONVERT(VARCHAR,@StartTime,120) + '</td></tr>
            <tr><td><b>End Time:</b></td><td>' + CONVERT(VARCHAR,@EndTime,120) + '</td></tr>
            <tr><td><b>Duration (sec):</b></td><td>' + ISNULL(CONVERT(VARCHAR,@Duration),'0') + '</td></tr>
            <tr><td><b>Error:</b></td><td>' + ISNULL(@ErrorMessage,'None') + '</td></tr>
        </table>

        <br>
        <b>Details:</b>
        <div style="padding-left:10px;">' + @DetailedMessage + '</div>

        <br>
        <b>Action Required:</b>
        <div style="padding-left:10px; color:#b30000;">' + @ActionRequired + '</div>

        <br><hr>
        <i style="color:#666;">This message was generated automatically by the DBAOps Notification Engine.</i>
    </body>
    </html>
    ';

    ----------------------------------------------
    -- 4. Send email using Database Mail
    ----------------------------------------------
    EXEC msdb.dbo.sp_send_dbmail
        @profile_name = @ProfileName,
        @recipients  = 'SQLServerAdminNotification@YOURORG.gov',
        @subject     = @Subject,
        @body        = @Body,
        @body_format = 'HTML';
END

GO

-- [Logging].[usp_CleanupLogs]

-- ============================================================================
-- Cleanup procedure for log retention
-- ============================================================================
CREATE   PROCEDURE Logging.usp_CleanupLogs
    @OperationLogRetentionDays INT = 30,
    @AlertLogRetentionDays INT = 90,
    @ErrorLogRetentionDays INT = 90
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @CutoffDate DATETIME2(3);
    DECLARE @Deleted INT;
    
    -- Cleanup OperationLog
    SET @CutoffDate = DATEADD(DAY, -@OperationLogRetentionDays, SYSDATETIME());
    DELETE FROM Logging.OperationLog WHERE LogTimestamp < @CutoffDate;
    SET @Deleted = @@ROWCOUNT;
    
    EXEC Logging.usp_WriteLog 
        @LogLevel = 'INFO',
        @Category = 'Maintenance',
        @Operation = 'Log Cleanup',
        @Message = 'Cleaned up OperationLog',
        @RowsAffected = @Deleted;
    
    -- Cleanup AlertLog
    SET @CutoffDate = DATEADD(DAY, -@AlertLogRetentionDays, SYSDATETIME());
    DELETE FROM Logging.AlertLog WHERE AlertTimestamp < @CutoffDate;
    SET @Deleted = @@ROWCOUNT;
    
    EXEC Logging.usp_WriteLog 
        @LogLevel = 'INFO',
        @Category = 'Maintenance',
        @Operation = 'Log Cleanup',
        @Message = 'Cleaned up AlertLog',
        @RowsAffected = @Deleted;
    
    -- Cleanup ErrorLog
    SET @CutoffDate = DATEADD(DAY, -@ErrorLogRetentionDays, SYSDATETIME());
    DELETE FROM Logging.ErrorLog WHERE ErrorTimestamp < @CutoffDate;
    SET @Deleted = @@ROWCOUNT;
    
    EXEC Logging.usp_WriteLog 
        @LogLevel = 'INFO',
        @Category = 'Maintenance',
        @Operation = 'Log Cleanup',
        @Message = 'Cleaned up ErrorLog',
        @RowsAffected = @Deleted;
END

GO

-- [Logging].[usp_UpdateAlertStatus]

-- ============================================================================
-- Procedure: Logging.usp_UpdateAlertStatus
-- Update alert delivery status
-- ============================================================================
CREATE   PROCEDURE Logging.usp_UpdateAlertStatus
    @AlertID BIGINT,
    @DeliveryStatus VARCHAR(20),
    @ErrorMessage NVARCHAR(2000) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    UPDATE Logging.AlertLog
    SET DeliveryStatus = @DeliveryStatus,
        DeliveryTimestamp = CASE WHEN @DeliveryStatus = 'Sent' THEN SYSDATETIME() ELSE DeliveryTimestamp END,
        ErrorMessage = @ErrorMessage,
        RetryCount = CASE WHEN @DeliveryStatus = 'Failed' THEN RetryCount + 1 ELSE RetryCount END
    WHERE AlertID = @AlertID;
END

GO

-- [Logging].[usp_WriteAlert]

-- ============================================================================
-- Procedure: Logging.usp_WriteAlert
-- Alert logging and tracking
-- ============================================================================
CREATE   PROCEDURE Logging.usp_WriteAlert
    @AlertType VARCHAR(50),
    @Severity VARCHAR(20),
    @Category VARCHAR(50),
    @Subject NVARCHAR(500),
    @Message NVARCHAR(MAX),
    @ServerName NVARCHAR(128) = NULL,
    @DatabaseName NVARCHAR(128) = NULL,
    @Recipients NVARCHAR(1000) = NULL,
    @SourceAlertRule NVARCHAR(128) = NULL,
    @CorrelationID UNIQUEIDENTIFIER = NULL,
    @AlertID BIGINT OUTPUT
AS
BEGIN
    SET NOCOUNT ON;
    
    INSERT INTO Logging.AlertLog (
        AlertType, Severity, Category, Subject, Message,
        ServerName, DatabaseName, Recipients, SourceAlertRule, CorrelationID
    )
    VALUES (
        @AlertType, @Severity, @Category, @Subject, @Message,
        @ServerName, @DatabaseName, @Recipients, @SourceAlertRule, @CorrelationID
    );
    
    SET @AlertID = SCOPE_IDENTITY();
END

GO

-- [Logging].[usp_WriteError]

-- ============================================================================
-- Procedure: Logging.usp_WriteError
-- Error logging with full context capture
-- ============================================================================
CREATE   PROCEDURE Logging.usp_WriteError
    @ErrorMessage NVARCHAR(4000) = NULL,
    @ServerName NVARCHAR(128) = NULL,
    @DatabaseName NVARCHAR(128) = NULL,
    @SourceJob NVARCHAR(128) = NULL,
    @SourceScript NVARCHAR(256) = NULL,
    @StackTrace NVARCHAR(MAX) = NULL,
    @InputParameters NVARCHAR(MAX) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    INSERT INTO Logging.ErrorLog (
        ErrorNumber, ErrorSeverity, ErrorState,
        ErrorProcedure, ErrorLine, ErrorMessage,
        ServerName, DatabaseName,
        SourceJob, SourceScript, StackTrace, InputParameters
    )
    VALUES (
        ERROR_NUMBER(),
        ERROR_SEVERITY(),
        ERROR_STATE(),
        ERROR_PROCEDURE(),
        ERROR_LINE(),
        COALESCE(@ErrorMessage, ERROR_MESSAGE()),
        @ServerName,
        @DatabaseName,
        @SourceJob,
        @SourceScript,
        @StackTrace,
        @InputParameters
    );
END

GO

-- [Logging].[usp_WriteLog]

-- ============================================================================
-- Procedure: Logging.usp_WriteLog
-- Standard logging procedure for all operations
-- ============================================================================
CREATE   PROCEDURE Logging.usp_WriteLog
    @LogLevel VARCHAR(20) = 'INFO',
    @Category VARCHAR(50),
    @Operation VARCHAR(100),
    @Message NVARCHAR(4000),
    @ServerName NVARCHAR(128) = NULL,
    @DatabaseName NVARCHAR(128) = NULL,
    @DurationMS INT = NULL,
    @RowsAffected INT = NULL,
    @SourceJob NVARCHAR(128) = NULL,
    @SourceScript NVARCHAR(256) = NULL,
    @AdditionalData NVARCHAR(MAX) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    INSERT INTO Logging.OperationLog (
        LogLevel, Category, Operation, Message, 
        ServerName, DatabaseName, DurationMS, RowsAffected,
        SourceJob, SourceScript, AdditionalData
    )
    VALUES (
        @LogLevel, @Category, @Operation, @Message,
        @ServerName, @DatabaseName, @DurationMS, @RowsAffected,
        @SourceJob, @SourceScript, @AdditionalData
    );
END

GO

-- [meta].[usp_BackupCompliance_DailyEmail]

CREATE PROCEDURE meta.usp_BackupCompliance_DailyEmail
    @OnlyOnFail BIT = 1  -- NEW: Only send if failures exist
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @ToList VARCHAR(300) = 'SQLServerAdminNotification@YOURORG.gov';
    DECLARE @Subject VARCHAR(200);
    DECLARE @Body NVARCHAR(MAX);
    
    -- Count failures
    DECLARE @FailCount INT = (SELECT COUNT(*) FROM fact.BackupCompliance WHERE Status = 'FAIL');
    
    -- =====================================================
    -- SMART EMAIL LOGIC: Only send if failures exist
    -- =====================================================
    IF @OnlyOnFail = 1 AND @FailCount = 0
    BEGIN
        PRINT 'DBAOps | JB: All backups compliant. Email not sent.';
        
        INSERT INTO meta.BackupComplianceLog(ServerName, Violations, EmailSentTo)
        SELECT 'ALL', 0, 'NOT SENT - All Compliant';
        RETURN;
    END;
    
    SET @Subject = 'DBAOps Backup Compliance - ' + CAST(@FailCount AS VARCHAR(10)) + ' SLA Violations';
    
    SET @Body = 
    N'<h2>DBAOps Backup Compliance Report</h2>' +
    N'<p>Total SLA Violations: <b style="color:red;">' + CAST(@FailCount AS VARCHAR(20)) + N'</b></p>' +
    N'<p><strong>This report only sent when backup failures exist.</strong></p>' +
    N'<table border="1" cellpadding="5" style="border-collapse:collapse;">' +
    N'<tr><th>Server</th><th>Database</th><th>Type</th><th>Last Backup</th><th>Age (min)</th><th>SLA</th><th>Status</th></tr>' +
    (
        SELECT
            '<tr><td>' + ServerName +
            '</td><td>' + DatabaseName +
            '</td><td>' + BackupType +
            '</td><td>' + ISNULL(CONVERT(VARCHAR, LastBackupTime, 120),'N/A') +
            '</td><td>' + CAST(AgeMinutes AS VARCHAR(20)) +
            '</td><td>' + CAST(SLA_Minutes AS VARCHAR(20)) +
            '</td><td style="color:red;font-weight:bold;">' + Status + '</td></tr>'
        FROM fact.BackupCompliance
        WHERE Status = 'FAIL'
        FOR XML PATH(''), TYPE
    ).value('.','nvarchar(max)')
    + N'</table>';
    
    BEGIN TRY
        EXEC msdb.dbo.sp_send_dbmail
            @recipients = @ToList,
            @subject = @Subject,
            @body = @Body,
            @body_format = 'HTML';
        
        INSERT INTO meta.BackupComplianceLog(ServerName, Violations, EmailSentTo)
        SELECT 'ALL', @FailCount, @ToList;
        
        PRINT 'DBAOps | JB: Backup compliance email sent - ' + CAST(@FailCount AS VARCHAR) + ' failures';
    END TRY
    BEGIN CATCH
        INSERT INTO meta.BackupComplianceLog(ServerName, Violations, EmailSentTo, ErrorMessage)
        SELECT 'ALL', @FailCount, @ToList, ERROR_MESSAGE();
    END CATCH
END;

GO

-- [meta].[usp_Fact_Report_Daily]

CREATE PROCEDURE meta.usp_Fact_Report_Daily
AS
BEGIN
    SET NOCOUNT ON;
    
    -- Generate daily FACT summary
    DECLARE @ServerCount INT, @DatabaseCount INT, @BackupCompliance DECIMAL(5,2);
    DECLARE @HealthyServers INT, @AlertCount INT;
    
    SELECT @ServerCount = COUNT(DISTINCT ServerName) FROM fact.BackupHealth;
    SELECT @DatabaseCount = COUNT(*) FROM fact.BackupHealth;
    SELECT @BackupCompliance = CAST(SUM(CASE WHEN IsCompliant = 1 THEN 1 ELSE 0 END) * 100.0 / NULLIF(COUNT(*), 0) AS DECIMAL(5,2))
    FROM fact.BackupHealth;
    
    SELECT @HealthyServers = COUNT(DISTINCT ServerName) 
    FROM metrics.FACT_ServerHealth 
    WHERE CaptureTime >= DATEADD(HOUR, -1, GETDATE())
      AND CPU_Utilization < 90 
      AND (Memory_UsedMB * 100.0 / NULLIF(Memory_TotalMB, 0)) < 90;
    
    SELECT @AlertCount = COUNT(*) 
    FROM alert.CriticalServerAlertLog 
    WHERE AlertSentAt >= DATEADD(DAY, -1, GETDATE());
    
    -- Log the report
    PRINT '========== DBAOps FACT Daily Report ==========';
    PRINT 'Report Date: ' + CONVERT(VARCHAR, GETDATE(), 120);
    PRINT 'Servers Monitored: ' + CAST(@ServerCount AS VARCHAR);
    PRINT 'Databases Tracked: ' + CAST(@DatabaseCount AS VARCHAR);
    PRINT 'Backup Compliance: ' + CAST(@BackupCompliance AS VARCHAR) + '%';
    PRINT 'Healthy Servers (1h): ' + CAST(@HealthyServers AS VARCHAR);
    PRINT 'Alerts (24h): ' + CAST(@AlertCount AS VARCHAR);
    PRINT '==============================================';
END

GO

-- [meta].[usp_Fact_Report_SendEmail]

CREATE PROCEDURE meta.usp_Fact_Report_SendEmail
    @ToList   VARCHAR(300),
    @Subject  VARCHAR(200),
    @Body     NVARCHAR(MAX)
AS
BEGIN
    SET NOCOUNT ON;

    BEGIN TRY
        EXEC msdb.dbo.sp_send_dbmail
            @recipients = @ToList,
            @subject    = @Subject,
            @body       = @Body,
            @body_format = 'HTML';

        INSERT INTO meta.FactReportLog (Status, SentTo)
        VALUES ('SUCCESS', @ToList);
    END TRY
    BEGIN CATCH
        INSERT INTO meta.FactReportLog (Status, SentTo, ErrorMessage)
        VALUES ('FAILED', @ToList, ERROR_MESSAGE());
    END CATCH
END;

GO

-- [metrics].[usp_CollectJobHistory]

-- Fix the procedure
CREATE PROCEDURE metrics.usp_CollectJobHistory
    @HoursBack INT = 24,
    @ServerName NVARCHAR(128) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @CutoffTime DATETIME = DATEADD(HOUR, -@HoursBack, GETDATE());
    DECLARE @Inserted INT = 0;
    
    -- Collect local job history
    INSERT INTO metrics.JobHistory (ServerName, JobName, StepName, RunDate, RunDuration, RunStatus, Message, CollectedAt)
    SELECT 
        @@SERVERNAME AS ServerName,
        j.name AS JobName,
        CASE h.step_id WHEN 0 THEN '(Job Outcome)' ELSE ISNULL(s.step_name, 'Step ' + CAST(h.step_id AS VARCHAR)) END AS StepName,
        msdb.dbo.agent_datetime(h.run_date, h.run_time) AS RunDate,
        h.run_duration AS RunDuration,
        CASE h.run_status 
            WHEN 0 THEN 'Failed'
            WHEN 1 THEN 'Succeeded'
            WHEN 2 THEN 'Retry'
            WHEN 3 THEN 'Canceled'
            WHEN 4 THEN 'In Progress'
            ELSE 'Unknown'
        END AS RunStatus,
        LEFT(h.message, 500) AS Message,
        GETDATE() AS CollectedAt
    FROM msdb.dbo.sysjobhistory h
    JOIN msdb.dbo.sysjobs j ON h.job_id = j.job_id
    LEFT JOIN msdb.dbo.sysjobsteps s ON h.job_id = s.job_id AND h.step_id = s.step_id
    WHERE msdb.dbo.agent_datetime(h.run_date, h.run_time) >= @CutoffTime
      AND (@ServerName IS NULL OR @@SERVERNAME = @ServerName)
      AND NOT EXISTS (
          SELECT 1 FROM metrics.JobHistory jh 
          WHERE jh.ServerName = @@SERVERNAME 
            AND jh.JobName = j.name 
            AND jh.RunDate = msdb.dbo.agent_datetime(h.run_date, h.run_time)
            AND jh.StepName = CASE h.step_id WHEN 0 THEN '(Job Outcome)' ELSE ISNULL(s.step_name, 'Step ' + CAST(h.step_id AS VARCHAR)) END
      );
    
    SET @Inserted = @@ROWCOUNT;
    PRINT 'Job history records inserted: ' + CAST(@Inserted AS VARCHAR(10));
END

GO

-- [metrics].[usp_InsertBaselineMetrics]

CREATE PROCEDURE [metrics].[usp_InsertBaselineMetrics]
    @ServerName NVARCHAR(128),
    @CPUPercent DECIMAL(5, 2), 
    @MemoryUsedMB BIGINT,
    @PLE INT,
    @WaitCategory NVARCHAR(128),
    @WaitSeconds DECIMAL(10, 2),
    @TotalMemoryMB BIGINT,
    @CollectionScope NVARCHAR(128)
AS
BEGIN
    SET NOCOUNT ON;
    
    -- 1. Insert the collected metrics
    INSERT INTO [metrics].[PerformanceBaseline] (
        [ServerName], [CPUPercent], [MemoryUsedMB], [PLE], [WaitCategory], 
        [WaitSeconds], [CollectedDate], [CaptureTime], [TotalMemoryMB], [CollectionScope]
    )
    VALUES (
        @ServerName, @CPUPercent, @MemoryUsedMB, @PLE, @WaitCategory, 
        @WaitSeconds, CAST(GETDATE() AS DATE), CAST(GETDATE() AS TIME), @TotalMemoryMB, @CollectionScope
    );

    -- 2. Compliance Check & Notification
    
    DECLARE @AlertMessage NVARCHAR(MAX);
    DECLARE @SubjectLine NVARCHAR(500); -- New variable for the subject line
    SET @AlertMessage = NULL;

    -- A. PLE Compliance Check
    IF @PLE IS NOT NULL AND @PLE < 300
    BEGIN
        SET @AlertMessage = CONCAT(
                              ISNULL(@AlertMessage, N''), 
                              N'<br>',                     
                              N'ðŸ”´ <b>LOW PLE ALERT:</b> Page Life Expectancy on ', 
                              @ServerName, 
                              N' is critically low: ', 
                              CAST(@PLE AS NVARCHAR(10)), 
                              N' seconds.'
                            );
    END

    -- B. CPU Compliance Check
    IF @CPUPercent IS NOT NULL AND @CPUPercent >= 90.00
    BEGIN
        SET @AlertMessage = CONCAT(
                              ISNULL(@AlertMessage, N''), 
                              N'<br>', 
                              N'ðŸ”¥ <b>HIGH CPU ALERT:</b> SQL Process CPU on ', 
                              @ServerName, 
                              N' is at ', 
                              CAST(@CPUPercent AS NVARCHAR(10)), 
                              N'%. Check activity immediately.'
                            );
    END

    -- C. Wait Stats Check
    IF @WaitCategory IS NOT NULL AND @WaitCategory NOT IN ('SLEEP_TASK', 'WAITFOR', 'LAZYWRITER_SLEEP') AND @WaitSeconds >= 5.00
    BEGIN
        SET @AlertMessage = CONCAT(
                              ISNULL(@AlertMessage, N''), 
                              N'<br>', 
                              N'âš ï¸ <b>HIGH WAIT ALERT:</b> Top Wait Category on ', 
                              @ServerName, 
                              N' is ', 
                              @WaitCategory, 
                              N' (Total Wait Time: ', 
                              CAST(@WaitSeconds AS NVARCHAR(10)), 
                              N' seconds).'
                            );
    END

    -- 3. Send Database Mail if any alert was triggered
    IF @AlertMessage IS NOT NULL
    BEGIN
        -- FIX: Pre-calculate the subject line to prevent syntax errors in the EXEC call
        SET @SubjectLine = CONCAT(N'ðŸ”´ Performance Baseline Compliance Alert: ', @ServerName);

        EXEC msdb.dbo.sp_send_dbmail
            @profile_name = N'DBAOpsProfile', 
            @recipients = N'dba_team@yourcompany.com', 
            @subject = @SubjectLine, -- Pass the pre-calculated variable
            @body = @AlertMessage,
            @body_format = N'HTML';
    END

END

GO

-- [metrics].[usp_UpdateForecastSimple]

CREATE PROCEDURE metrics.usp_UpdateForecastSimple
AS
BEGIN
    SET NOCOUNT ON;
    PRINT 'Forecast update: ' + CONVERT(VARCHAR, GETDATE(), 120);
    PRINT 'Complete';
END

GO

-- [Monitoring].[usp_AuditEvent_Apply]

CREATE   PROCEDURE Monitoring.usp_AuditEvent_Apply
AS
BEGIN
    SET NOCOUNT ON;

    ;WITH Filtered AS
    (
        SELECT *
        FROM Monitoring.Sql_AuditEvent_Staging
        WHERE 1 = 1
          -- Drop noisy session-init rows like the ones you saw (network protocol + SET options)
          AND NOT (
                Statement IS NOT NULL
                AND Statement LIKE N'-- network protocol: LPC%'
                AND Statement LIKE N'%set quoted_identifier on%'
                AND Statement LIKE N'%set ansi_warnings on%'
            )
    )
    INSERT Monitoring.Sql_AuditEvent
    (
        ServerName, EventTimeUtc, LoginName, DatabaseName, ObjectName,
        ActionId, ClientIp, HostName, Statement, SourceLog
    )
    SELECT
        ServerName, EventTimeUtc, LoginName, DatabaseName, ObjectName,
        ActionId, ClientIp, HostName, Statement, SourceLog
    FROM Filtered;

    DELETE S
    FROM Monitoring.Sql_AuditEvent_Staging AS S;
END;

GO

-- [Monitoring].[usp_AuditEvent_Ingest]

CREATE   PROCEDURE Monitoring.usp_AuditEvent_Ingest
(
    @ServerName   sysname,
    @EventTimeUtc datetime2(0),
    @LoginName    nvarchar(256),
    @DatabaseName sysname         = NULL,
    @ObjectName   nvarchar(512)   = NULL,
    @ActionId     nvarchar(4)     = NULL,
    @ClientIp     nvarchar(64)    = NULL,
    @HostName     nvarchar(256)   = NULL,
    @Statement    nvarchar(max)   = NULL,
    @SourceLog    nvarchar(32)    = N'Application'
)
AS
BEGIN
    SET NOCOUNT ON;

    INSERT Monitoring.Sql_AuditEvent_Staging
    (
        ServerName, EventTimeUtc, LoginName, DatabaseName, ObjectName,
        ActionId, ClientIp, HostName, Statement, SourceLog
    )
    VALUES
    (
        @ServerName, @EventTimeUtc, @LoginName, @DatabaseName, @ObjectName,
        @ActionId, @ClientIp, @HostName, @Statement, @SourceLog
    );
END;

GO

-- [Monitoring].[usp_AuditEvent_Purge]

CREATE   PROCEDURE Monitoring.usp_AuditEvent_Purge
(
    @RetentionDays int = 180
)
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @Cutoff datetime2(0) = DATEADD(DAY, -@RetentionDays, SYSUTCDATETIME());

    DELETE TOP (10000) AE
    FROM Monitoring.Sql_AuditEvent AS AE
    WHERE AE.EventTimeUtc < @Cutoff;
END;

GO

-- [Monitoring].[usp_GetJobComplianceDashboard]

-- ============================================================================
-- STORED PROCEDURE: monitoring.usp_GetJobComplianceDashboard
-- Returns data for dashboard display
-- ============================================================================
CREATE   PROCEDURE monitoring.usp_GetJobComplianceDashboard
AS
BEGIN
    SET NOCOUNT ON;

    -- Summary counts
    SELECT 
        COUNT(DISTINCT ServerName) AS TotalServers,
        SUM(CASE WHEN ComplianceStatus = 'Compliant' THEN 1 ELSE 0 END) AS CompliantCount,
        SUM(CASE WHEN ComplianceStatus = 'NonCompliant' THEN 1 ELSE 0 END) AS NonCompliantCount,
        SUM(CASE WHEN ComplianceStatus = 'Fixed' THEN 1 ELSE 0 END) AS FixedCount,
        SUM(CASE WHEN ComplianceStatus = 'FixFailed' THEN 1 ELSE 0 END) AS FixFailedCount,
        SUM(CASE WHEN ComplianceStatus = 'Unreachable' THEN 1 ELSE 0 END) AS UnreachableCount
    FROM monitoring.JobComplianceCurrentStatus;

    -- Non-compliant servers (for attention)
    SELECT 
        cs.ServerName,
        cs.JobName,
        d.RuleName,
        d.Severity,
        cs.ComplianceStatus,
        cs.IssueDescription,
        cs.LastCheckedDate
    FROM monitoring.JobComplianceCurrentStatus cs
    JOIN monitoring.JobComplianceDefinitions d ON cs.DefinitionID = d.DefinitionID
    WHERE cs.ComplianceStatus IN ('NonCompliant', 'FixFailed')
    ORDER BY 
        CASE d.Severity WHEN 'Critical' THEN 1 WHEN 'Warning' THEN 2 ELSE 3 END,
        cs.ServerName;

    -- Last scan info
    SELECT TOP 1
        ScanID,
        ScanStartTime,
        ScanEndTime,
        ServersScanned,
        ServersAffected,
        ServersFixed,
        ServersFailed,
        ScanStatus
    FROM monitoring.JobComplianceScanHistory
    ORDER BY ScanStartTime DESC;
END

GO

-- [Monitoring].[usp_RecordJobComplianceResult]

-- ============================================================================
-- STORED PROCEDURE: monitoring.usp_RecordJobComplianceResult
-- Records individual server check result
-- ============================================================================
CREATE   PROCEDURE monitoring.usp_RecordJobComplianceResult
    @ScanID INT,
    @DefinitionID INT,
    @ServerName NVARCHAR(256),
    @JobName NVARCHAR(256),
    @StepID INT = NULL,
    @StepName NVARCHAR(256) = NULL,
    @ComplianceStatus NVARCHAR(20),
    @IssueDescription NVARCHAR(500) = NULL,
    @FixApplied BIT = 0,
    @FixResult NVARCHAR(500) = NULL
AS
BEGIN
    SET NOCOUNT ON;

    -- Insert into history
    INSERT INTO monitoring.JobComplianceResults (
        ScanID,
        DefinitionID,
        ServerName,
        JobName,
        StepID,
        StepName,
        ComplianceStatus,
        IssueDescription,
        FixApplied,
        FixResult,
        FixedDate
    )
    VALUES (
        @ScanID,
        @DefinitionID,
        @ServerName,
        @JobName,
        @StepID,
        @StepName,
        @ComplianceStatus,
        @IssueDescription,
        @FixApplied,
        @FixResult,
        CASE WHEN @FixApplied = 1 THEN GETDATE() ELSE NULL END
    );

    -- Update current status (upsert)
    MERGE monitoring.JobComplianceCurrentStatus AS target
    USING (SELECT @ServerName AS ServerName, @JobName AS JobName, @DefinitionID AS DefinitionID) AS source
    ON target.ServerName = source.ServerName 
       AND target.JobName = source.JobName 
       AND target.DefinitionID = source.DefinitionID
    WHEN MATCHED THEN
        UPDATE SET 
            ComplianceStatus = @ComplianceStatus,
            LastCheckedDate = GETDATE(),
            LastFixedDate = CASE WHEN @FixApplied = 1 THEN GETDATE() ELSE target.LastFixedDate END,
            IssueDescription = @IssueDescription
    WHEN NOT MATCHED THEN
        INSERT (ServerName, JobName, DefinitionID, ComplianceStatus, LastCheckedDate, LastFixedDate, IssueDescription)
        VALUES (@ServerName, @JobName, @DefinitionID, @ComplianceStatus, GETDATE(), 
                CASE WHEN @FixApplied = 1 THEN GETDATE() ELSE NULL END, @IssueDescription);
END

GO

-- [Monitoring].[usp_RecordJobComplianceScan]

-- ============================================================================
-- STORED PROCEDURE: monitoring.usp_RecordJobComplianceScan
-- Records a new scan and returns the ScanID
-- ============================================================================
CREATE   PROCEDURE monitoring.usp_RecordJobComplianceScan
    @AutoFixEnabled BIT = 0,
    @ScanID INT OUTPUT
AS
BEGIN
    SET NOCOUNT ON;

    INSERT INTO monitoring.JobComplianceScanHistory (
        ScanStartTime,
        AutoFixEnabled,
        ScanStatus
    )
    VALUES (
        GETDATE(),
        @AutoFixEnabled,
        'Running'
    );

    SET @ScanID = SCOPE_IDENTITY();
END

GO

-- [Monitoring].[usp_UpdateJobComplianceScan]

-- ============================================================================
-- STORED PROCEDURE: monitoring.usp_UpdateJobComplianceScan
-- Updates scan completion status
-- ============================================================================
CREATE   PROCEDURE monitoring.usp_UpdateJobComplianceScan
    @ScanID INT,
    @ServersScanned INT,
    @ServersAffected INT,
    @ServersFixed INT,
    @ServersFailed INT,
    @ServersUnreachable INT,
    @ScanStatus NVARCHAR(20),
    @ErrorMessage NVARCHAR(MAX) = NULL
AS
BEGIN
    SET NOCOUNT ON;

    UPDATE monitoring.JobComplianceScanHistory
    SET 
        ScanEndTime = GETDATE(),
        ServersScanned = @ServersScanned,
        ServersAffected = @ServersAffected,
        ServersFixed = @ServersFixed,
        ServersFailed = @ServersFailed,
        ServersUnreachable = @ServersUnreachable,
        ScanStatus = @ScanStatus,
        ErrorMessage = @ErrorMessage
    WHERE ScanID = @ScanID;
END

GO

-- [Patching].[usp_AddServerToPatchGroup]

-- ============================================================
-- 9. ADD SERVER TO PATCH GROUP
-- ============================================================
CREATE   PROCEDURE Patching.usp_AddServerToPatchGroup
    @ServerName VARCHAR(128),
    @GroupID INT,
    @PatchOrder INT = 1
AS
BEGIN
    SET NOCOUNT ON;
    
    -- Verify server exists
    IF NOT EXISTS (SELECT 1 FROM config.ServerInventory WHERE ServerName = @ServerName AND IsActive = 1)
    BEGIN
        RAISERROR('Server not found in inventory', 16, 1);
        RETURN -1;
    END
    
    -- Check if already in group
    IF EXISTS (SELECT 1 FROM Patching.ServerGroupMembers WHERE ServerName = @ServerName AND GroupID = @GroupID)
    BEGIN
        UPDATE Patching.ServerGroupMembers
        SET IsActive = 1, PatchOrder = @PatchOrder
        WHERE ServerName = @ServerName AND GroupID = @GroupID;
    END
    ELSE
    BEGIN
        INSERT INTO Patching.ServerGroupMembers (GroupID, ServerName, PatchOrder)
        VALUES (@GroupID, @ServerName, @PatchOrder);
    END
    
    PRINT 'Server ' + @ServerName + ' added to group ' + CAST(@GroupID AS VARCHAR);
    RETURN 0;
END

GO

-- [Patching].[usp_AddToQueue]

-- â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
-- SECTION 10: PROCEDURE - ADD TO QUEUE
-- â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
CREATE   PROCEDURE Patching.usp_AddToQueue
    @ServerList NVARCHAR(MAX),
    @SQLVersion NVARCHAR(20) = '2022',
    @ScheduledDate DATETIME = NULL,
    @MaintenanceWindow NVARCHAR(50) = NULL,
    @AutoApprove BIT = 0,
    @Priority INT = 50
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @PackageID INT;
    DECLARE @ServerName NVARCHAR(128);
    DECLARE @Added INT = 0;
    DECLARE @Skipped INT = 0;
    
    SELECT TOP 1 @PackageID = PackageID 
    FROM Patching.CUPackages 
    WHERE SQLVersion = @SQLVersion AND IsLatest = 1 AND IsActive = 1
    ORDER BY ReleaseDate DESC;
    
    IF @PackageID IS NULL
    BEGIN
        RAISERROR('No active CU package found for SQL Server %s', 16, 1, @SQLVersion);
        RETURN;
    END
    
    DECLARE server_cursor CURSOR FOR
        SELECT LTRIM(RTRIM(value)) FROM STRING_SPLIT(@ServerList, ',') WHERE LTRIM(RTRIM(value)) <> '';
    
    OPEN server_cursor;
    FETCH NEXT FROM server_cursor INTO @ServerName;
    
    WHILE @@FETCH_STATUS = 0
    BEGIN
        IF NOT EXISTS (SELECT 1 FROM Patching.DeploymentQueue WHERE ServerName = @ServerName AND Status IN ('Pending', 'Approved', 'InProgress'))
        BEGIN
            INSERT INTO Patching.DeploymentQueue (ServerName, PackageID, Priority, ScheduledDate, MaintenanceWindow, Status, ApprovedBy, ApprovedDate)
            VALUES (@ServerName, @PackageID, @Priority, @ScheduledDate, @MaintenanceWindow,
                    CASE WHEN @AutoApprove = 1 THEN 'Approved' ELSE 'Pending' END,
                    CASE WHEN @AutoApprove = 1 THEN SUSER_SNAME() ELSE NULL END,
                    CASE WHEN @AutoApprove = 1 THEN GETDATE() ELSE NULL END);
            SET @Added = @Added + 1;
            PRINT '  âœ“ Added: ' + @ServerName;
        END
        ELSE
        BEGIN
            SET @Skipped = @Skipped + 1;
            PRINT '  â—‹ Skipped (already in queue): ' + @ServerName;
        END
        
        FETCH NEXT FROM server_cursor INTO @ServerName;
    END
    
    CLOSE server_cursor;
    DEALLOCATE server_cursor;
    
    PRINT '';
    PRINT '  Summary: ' + CAST(@Added AS VARCHAR) + ' added, ' + CAST(@Skipped AS VARCHAR) + ' skipped';
END;

GO

-- [Patching].[usp_ApprovePatch]

-- ============================================================
-- 2. APPROVE A PATCH
-- ============================================================
CREATE   PROCEDURE Patching.usp_ApprovePatch
    @ScheduleID INT,
    @ApprovedBy VARCHAR(100) = NULL,
    @Comments VARCHAR(500) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    IF NOT EXISTS (SELECT 1 FROM Patching.ScheduledPatches WHERE ScheduleID = @ScheduleID)
    BEGIN
        RAISERROR('Schedule not found', 16, 1);
        RETURN -1;
    END
    
    UPDATE Patching.ScheduledPatches
    SET ApprovalStatus = 'Approved',
        ApprovedBy = ISNULL(@ApprovedBy, SUSER_SNAME()),
        ApprovedDate = GETDATE(),
        Status = 'Approved',
        ModifiedBy = SUSER_SNAME(),
        ModifiedDate = GETDATE()
    WHERE ScheduleID = @ScheduleID;
    
    PRINT 'Patch approved: ID=' + CAST(@ScheduleID AS VARCHAR);
    RETURN 0;
END

GO

-- [Patching].[usp_ApprovePatchRequest]

CREATE PROCEDURE Patching.usp_ApprovePatchRequest
    @RequestID INT,
    @ApprovedBy VARCHAR(100) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    UPDATE Patching.PatchRequests
    SET Status = 'Approved',
        ApprovedBy = ISNULL(@ApprovedBy, SYSTEM_USER),
        ApprovedDate = GETDATE()
    WHERE RequestID = @RequestID AND Status = 'Pending';
    
    SELECT @@ROWCOUNT AS RowsAffected;
END

GO

-- [Patching].[usp_ApproveQueue]

-- â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
-- SECTION 11: PROCEDURE - APPROVE QUEUE
-- â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
CREATE   PROCEDURE Patching.usp_ApproveQueue
    @QueueID INT = NULL,
    @ServerName NVARCHAR(128) = NULL,
    @ApproveAll BIT = 0
AS
BEGIN
    SET NOCOUNT ON;
    
    UPDATE Patching.DeploymentQueue
    SET Status = 'Approved', ApprovedBy = SUSER_SNAME(), ApprovedDate = GETDATE()
    WHERE Status = 'Pending'
      AND (@ApproveAll = 1 OR QueueID = @QueueID OR ServerName = @ServerName);
    
    PRINT '  âœ“ Approved ' + CAST(@@ROWCOUNT AS VARCHAR) + ' deployment(s)';
END;

GO

-- [Patching].[usp_ApproveRelease]

-- â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
-- Procedure: usp_ApproveRelease
-- Purpose: Apply a detected release after DBA approval
-- â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
CREATE PROCEDURE Patching.usp_ApproveRelease
    @DetectionID INT = NULL,
    @SQLMajorVersion INT = NULL,
    @ReleaseName NVARCHAR(20) = NULL,
    @KBNumber NVARCHAR(20) = NULL,
    @BuildNumber NVARCHAR(20) = NULL,
    @ReleaseDate DATE = NULL,
    @DownloadURL NVARCHAR(500) = NULL,
    @SecurityFixes INT = 0,
    @BugFixes INT = 0,
    @IsCritical BIT = 0,
    @ReleaseNotes NVARCHAR(MAX) = NULL,
    @ApprovedBy NVARCHAR(100) = NULL
AS
BEGIN
    /*
    â•”â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•—
    â•‘ DBAOps Monitoring and Compliance Framework                                â•‘
    â•‘ Procedure: usp_ApproveRelease                                             â•‘
    â•‘ Author: John Bosco RHOYLLE INNOVATION EMPIRE™, Sr SQL DBA/DBAOps Engineer                   â•‘
    â•‘                                                                           â•‘
    â•‘ Purpose: Apply a detected release after DBA approval                      â•‘
    â•‘ Usage:   Called by DBA after reviewing notification email                 â•‘
    â•šâ•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
    */
    SET NOCOUNT ON;
    
    DECLARE @SQLVersionName NVARCHAR(50);
    
    -- If DetectionID provided, get values from detection log
    IF @DetectionID IS NOT NULL
    BEGIN
        SELECT 
            @SQLMajorVersion = SQLMajorVersion,
            @SQLVersionName = SQLVersionName,
            @ReleaseName = DetectedRelease,
            @KBNumber = DetectedKB,
            @BuildNumber = DetectedBuild,
            @ReleaseDate = DetectedReleaseDate
        FROM Patching.ReleaseDetectionLog
        WHERE DetectionID = @DetectionID;
    END
    ELSE
    BEGIN
        SET @SQLVersionName = CASE @SQLMajorVersion
            WHEN 17 THEN 'SQL Server 2025'
            WHEN 16 THEN 'SQL Server 2022'
            WHEN 15 THEN 'SQL Server 2019'
            WHEN 14 THEN 'SQL Server 2017'
            WHEN 13 THEN 'SQL Server 2016'
            ELSE 'Unknown'
        END;
    END
    
    IF @ApprovedBy IS NULL
        SET @ApprovedBy = SYSTEM_USER;
    
    BEGIN TRY
        BEGIN TRANSACTION;
        
        -- 1. Update MicrosoftReleases table (mark previous as not latest)
        UPDATE Patching.MicrosoftReleases 
        SET IsLatest = 0 
        WHERE SQLMajorVersion = @SQLMajorVersion 
          AND ReleaseType = CASE WHEN @ReleaseName LIKE 'CU%' THEN 'CU' WHEN @ReleaseName LIKE 'GDR%' THEN 'GDR' ELSE 'SP' END;
        
        -- Insert or update the new release
        IF EXISTS (SELECT 1 FROM Patching.MicrosoftReleases WHERE SQLMajorVersion = @SQLMajorVersion AND KBNumber = @KBNumber)
        BEGIN
            UPDATE Patching.MicrosoftReleases
            SET ReleaseName = @ReleaseName,
                BuildNumber = @BuildNumber,
                ReleaseDate = @ReleaseDate,
                DownloadURL = @DownloadURL,
                SecurityFixes = @SecurityFixes,
                BugFixes = @BugFixes,
                IsCritical = @IsCritical,
                ReleaseNotes = @ReleaseNotes,
                IsLatest = 1
            WHERE SQLMajorVersion = @SQLMajorVersion AND KBNumber = @KBNumber;
        END
        ELSE
        BEGIN
            INSERT INTO Patching.MicrosoftReleases 
            (SQLMajorVersion, SQLVersionName, ReleaseType, ReleaseName, KBNumber, BuildNumber, ReleaseDate, DownloadURL, SecurityFixes, BugFixes, IsCritical, IsLatest, ReleaseNotes)
            VALUES 
            (@SQLMajorVersion, @SQLVersionName, 
             CASE WHEN @ReleaseName LIKE 'CU%' THEN 'CU' WHEN @ReleaseName LIKE 'GDR%' THEN 'GDR' ELSE 'SP' END,
             @ReleaseName, @KBNumber, @BuildNumber, @ReleaseDate, @DownloadURL, @SecurityFixes, @BugFixes, @IsCritical, 1, @ReleaseNotes);
        END
        
        -- 2. Update SQLServerBuildReference table
        UPDATE Patching.SQLServerBuildReference
        SET LatestCU = @ReleaseName,
            LatestBuildNumber = @BuildNumber,
            KBNumber = @KBNumber,
            DownloadURL = @DownloadURL,
            ReleaseDate = @ReleaseDate,
            LastUpdated = GETDATE()
        WHERE SQLMajorVersion = @SQLMajorVersion;
        
        -- 3. Add to KnownBuilds if not exists
        IF NOT EXISTS (SELECT 1 FROM Patching.KnownBuilds WHERE SQLMajorVersion = @SQLMajorVersion AND BuildNumber = @BuildNumber)
        BEGIN
            INSERT INTO Patching.KnownBuilds (SQLMajorVersion, SQLVersionName, ReleaseType, ReleaseName, KBNumber, BuildNumber, ReleaseDate)
            VALUES (@SQLMajorVersion, @SQLVersionName, 
                    CASE WHEN @ReleaseName LIKE 'CU%' THEN 'CU' WHEN @ReleaseName LIKE 'GDR%' THEN 'GDR' ELSE 'SP' END,
                    @ReleaseName, @KBNumber, @BuildNumber, @ReleaseDate);
        END
        
        -- 4. Update detection log if DetectionID provided
        IF @DetectionID IS NOT NULL
        BEGIN
            UPDATE Patching.ReleaseDetectionLog
            SET ApprovedBy = @ApprovedBy,
                ApprovedDate = GETDATE(),
                AppliedDate = GETDATE(),
                Notes = 'Approved and applied by ' + @ApprovedBy
            WHERE DetectionID = @DetectionID;
        END
        
        COMMIT TRANSACTION;
        
        PRINT 'â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•';
        PRINT '  âœ“ RELEASE APPROVED AND APPLIED SUCCESSFULLY';
        PRINT 'â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•';
        PRINT '  SQL Version:    ' + @SQLVersionName;
        PRINT '  Release:        ' + @ReleaseName;
        PRINT '  KB Number:      ' + @KBNumber;
        PRINT '  Build:          ' + @BuildNumber;
        PRINT '  Approved By:    ' + @ApprovedBy;
        PRINT '  Approved Date:  ' + CONVERT(VARCHAR, GETDATE(), 120);
        PRINT 'â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•';
        PRINT '';
        PRINT '  The weekly Monday report will now show servers needing this update.';
        PRINT '  Non-Prod patching: Thursday of Patch Tuesday week';
        PRINT '  Production patching: Following Thursday';
        PRINT 'â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•';
        
        -- Return success
        SELECT 
            'SUCCESS' AS Status,
            @SQLVersionName AS SQLVersion,
            @ReleaseName AS Release,
            @KBNumber AS KBNumber,
            @BuildNumber AS Build,
            @ApprovedBy AS ApprovedBy,
            GETDATE() AS ApprovedDate;
            
    END TRY
    BEGIN CATCH
        IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION;
        
        PRINT 'ERROR: ' + ERROR_MESSAGE();
        
        SELECT 
            'FAILED' AS Status,
            ERROR_MESSAGE() AS ErrorMessage;
    END CATCH
END

GO

-- [Patching].[usp_BulkSchedulePatches]

CREATE PROCEDURE Patching.usp_BulkSchedulePatches
    @ServerList NVARCHAR(MAX),
    @ScheduledDate DATETIME,
    @MaintenanceWindowID INT = NULL,
    @PatchType NVARCHAR(50) = 'CU',
    @Priority NVARCHAR(20) = 'Normal',
    @ChangeTicket NVARCHAR(50) = NULL,
    @Notes NVARCHAR(1000) = NULL,
    @CreatedBy NVARCHAR(100) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @Scheduled INT = 0;
    DECLARE @ServerName NVARCHAR(128);
    
    CREATE TABLE #Servers (ServerName NVARCHAR(128));
    
    INSERT INTO #Servers (ServerName)
    SELECT LTRIM(RTRIM(value)) 
    FROM STRING_SPLIT(@ServerList, ',')
    WHERE LTRIM(RTRIM(value)) <> '';
    
    DECLARE server_cursor CURSOR FOR
        SELECT ServerName FROM #Servers;
    
    OPEN server_cursor;
    FETCH NEXT FROM server_cursor INTO @ServerName;
    
    WHILE @@FETCH_STATUS = 0
    BEGIN
        EXEC Patching.usp_SchedulePatch 
            @ServerName = @ServerName,
            @ScheduledDate = @ScheduledDate,
            @MaintenanceWindowID = @MaintenanceWindowID,
            @PatchType = @PatchType,
            @Priority = @Priority,
            @ChangeTicket = @ChangeTicket,
            @Notes = @Notes,
            @CreatedBy = @CreatedBy;
        
        SET @Scheduled = @Scheduled + 1;
        FETCH NEXT FROM server_cursor INTO @ServerName;
    END
    
    CLOSE server_cursor;
    DEALLOCATE server_cursor;
    
    DROP TABLE #Servers;
    
    SELECT @Scheduled AS ServersScheduled;
END

GO

-- [Patching].[usp_CancelPatch]

-- ============================================================
-- 3. CANCEL A PATCH
-- ============================================================
CREATE   PROCEDURE Patching.usp_CancelPatch
    @ScheduleID INT,
    @CancelReason VARCHAR(500) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    UPDATE Patching.ScheduledPatches
    SET Status = 'Cancelled',
        ExecutionLog = ISNULL(ExecutionLog, '') + CHAR(13) + CHAR(10) + 
            'Cancelled by ' + SUSER_SNAME() + ' at ' + CONVERT(VARCHAR, GETDATE(), 120) +
            ISNULL(': ' + @CancelReason, ''),
        ModifiedBy = SUSER_SNAME(),
        ModifiedDate = GETDATE()
    WHERE ScheduleID = @ScheduleID;
    
    PRINT 'Patch cancelled: ID=' + CAST(@ScheduleID AS VARCHAR);
    RETURN 0;
END

GO

-- [Patching].[usp_CheckAndNotifyNewReleases]

CREATE   PROCEDURE [Patching].[usp_CheckAndNotifyNewReleases]
AS
BEGIN
    SET NOCOUNT ON;
    
    -- Get servers that need the new release
    SELECT 
        cms.ServerName,
        COALESCE(cms.Environment, cms.CMSEnvironment, 'Unknown') AS Environment,
        cms.SQLVersion,
        cms.SqlCumulativeUpdate AS CurrentCU,
        br.LatestCU AS NewCU,
        br.KBNumber,
        br.ReleaseDate
    FROM config.ServerInventory cms
    INNER JOIN Patching.SQLServerBuildReference br 
        ON cms.SQLVersion LIKE CAST(br.SQLMajorVersion AS VARCHAR) + '.%'
    WHERE cms.IsActive = 1
    AND (cms.ServerType = 'SQL Server' OR cms.ServerType IS NULL)
    AND (cms.SqlCumulativeUpdate <> br.LatestCU OR cms.SqlCumulativeUpdate IS NULL)
    AND br.IsSupported = 1
    ORDER BY cms.Environment, cms.ServerName;
END

GO

-- [Patching].[usp_CheckTaniumJobsStatus]

-- â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
-- Procedure: Patching.usp_CheckTaniumJobsStatus
-- Purpose: Check SQL Agent job status
-- Author: John Bosco RHOYLLE INNOVATION EMPIRE™
-- â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
CREATE PROCEDURE Patching.usp_CheckTaniumJobsStatus
AS
BEGIN
    /*
    â•”â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•—
    â•‘ DBAOps Monitoring and Compliance Framework                                â•‘
    â•‘ Procedure: usp_CheckTaniumJobsStatus                                      â•‘
    â•‘ Author: John Bosco RHOYLLE INNOVATION EMPIRE™, Sr SQL DBA/DBAOps Engineer                   â•‘
    â•šâ•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
    */
    SET NOCOUNT ON;
    
    TRUNCATE TABLE Patching.TaniumJobStatus;
    
    INSERT INTO Patching.TaniumJobStatus (JobName, JobEnabled, LastRunDate, LastRunStatus, LastRunDuration, NextRunDate, ScheduleEnabled)
    SELECT j.name, j.enabled,
        CASE WHEN h.run_date IS NOT NULL THEN msdb.dbo.agent_datetime(h.run_date, h.run_time) ELSE NULL END,
        CASE h.run_status WHEN 0 THEN 'Failed' WHEN 1 THEN 'Succeeded' WHEN 2 THEN 'Retry' WHEN 3 THEN 'Canceled' ELSE 'Unknown' END,
        h.run_duration,
        CASE WHEN js.next_run_date > 0 THEN msdb.dbo.agent_datetime(js.next_run_date, js.next_run_time) ELSE NULL END,
        s.enabled
    FROM msdb.dbo.sysjobs j
    LEFT JOIN (SELECT job_id, run_date, run_time, run_status, run_duration, ROW_NUMBER() OVER (PARTITION BY job_id ORDER BY run_date DESC, run_time DESC) rn FROM msdb.dbo.sysjobhistory WHERE step_id = 0) h ON j.job_id = h.job_id AND h.rn = 1
    LEFT JOIN msdb.dbo.sysjobschedules js ON j.job_id = js.job_id
    LEFT JOIN msdb.dbo.sysschedules s ON js.schedule_id = s.schedule_id
    WHERE j.name LIKE '%Patch%' OR j.name LIKE '%Compliance%';
    
    SELECT JobName, CASE JobEnabled WHEN 1 THEN 'Enabled' ELSE 'DISABLED' END AS Status, LastRunDate, LastRunStatus, NextRunDate,
        CASE WHEN JobEnabled = 0 THEN 'CRITICAL' WHEN LastRunStatus = 'Failed' THEN 'ERROR' ELSE 'OK' END AS Health
    FROM Patching.TaniumJobStatus ORDER BY JobName;
END

GO

-- [Patching].[usp_CollectAllServerPatchInfo]

CREATE PROCEDURE Patching.usp_CollectAllServerPatchInfo
    @SpecificServer NVARCHAR(128) = NULL  -- NULL = all servers
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @ServerName NVARCHAR(128);
    DECLARE @SQL NVARCHAR(MAX);
    DECLARE @ErrorMsg NVARCHAR(MAX);
    DECLARE @SuccessCount INT = 0;
    DECLARE @FailCount INT = 0;
    
    -- Build reference data
    DECLARE @BuildRef TABLE (
        MajorVersion INT,
        VersionName NVARCHAR(50),
        LatestCU NVARCHAR(20),
        LatestBuild NVARCHAR(20)
    );
    
    INSERT INTO @BuildRef VALUES
        (11, 'SQL Server 2012', 'SP4', '11.0.7507.2'),
        (12, 'SQL Server 2014', 'SP3-CU4', '12.0.6449.1'),
        (13, 'SQL Server 2016', 'SP3', '13.0.6441.1'),
        (14, 'SQL Server 2017', 'CU31', '14.0.3485.1'),
        (15, 'SQL Server 2019', 'CU29', '15.0.4395.2'),
        (16, 'SQL Server 2022', 'CU16', '16.0.4165.4'),
        (17, 'SQL Server 2025', 'RTM', '17.0.1000.0');
    
    -- Cursor through servers
    DECLARE server_cursor CURSOR FOR
        SELECT ServerName 
        FROM Patching.TaniumPatchStatus 
        WHERE IsActive = 1
          AND (@SpecificServer IS NULL OR ServerName = @SpecificServer)
        ORDER BY ServerName;
    
    OPEN server_cursor;
    FETCH NEXT FROM server_cursor INTO @ServerName;
    
    WHILE @@FETCH_STATUS = 0
    BEGIN
        BEGIN TRY
            -- Create temp table for results
            IF OBJECT_ID('tempdb..#ServerInfo') IS NOT NULL DROP TABLE #ServerInfo;
            CREATE TABLE #ServerInfo (
                ServerName NVARCHAR(128),
                ProductVersion NVARCHAR(50),
                ProductLevel NVARCHAR(50),
                ProductUpdateLevel NVARCHAR(50),
                Edition NVARCHAR(128),
                MajorVersion INT
            );
            
            -- Query remote server using OPENROWSET or linked server
            -- Try direct connection first
            SET @SQL = N'
            BEGIN TRY
                INSERT INTO #ServerInfo
                SELECT 
                    @@SERVERNAME,
                    CAST(SERVERPROPERTY(''ProductVersion'') AS NVARCHAR(50)),
                    CAST(SERVERPROPERTY(''ProductLevel'') AS NVARCHAR(50)),
                    CAST(SERVERPROPERTY(''ProductUpdateLevel'') AS NVARCHAR(50)),
                    CAST(SERVERPROPERTY(''Edition'') AS NVARCHAR(128)),
                    CAST(SERVERPROPERTY(''ProductMajorVersion'') AS INT)
                FROM OPENROWSET(
                    ''SQLNCLI'', 
                    ''Server=' + @ServerName + ';Trusted_Connection=yes;'',
                    ''SELECT 1''
                ) AS a;
            END TRY
            BEGIN CATCH
                -- Connection failed
                SELECT NULL;
            END CATCH';
            
            -- Alternative: Use sp_executesql with linked server if available
            -- Check if linked server exists
            IF EXISTS (SELECT 1 FROM sys.servers WHERE name = @ServerName)
            BEGIN
                SET @SQL = N'
                INSERT INTO #ServerInfo
                EXEC(''
                    SELECT 
                        @@SERVERNAME,
                        CAST(SERVERPROPERTY(''''ProductVersion'''') AS NVARCHAR(50)),
                        CAST(SERVERPROPERTY(''''ProductLevel'''') AS NVARCHAR(50)),
                        CAST(SERVERPROPERTY(''''ProductUpdateLevel'''') AS NVARCHAR(50)),
                        CAST(SERVERPROPERTY(''''Edition'''') AS NVARCHAR(128)),
                        CAST(SERVERPROPERTY(''''ProductMajorVersion'''') AS INT)
                '') AT [' + @ServerName + ']';
                
                EXEC sp_executesql @SQL;
            END
            ELSE
            BEGIN
                -- Try OPENROWSET
                SET @SQL = N'
                INSERT INTO #ServerInfo
                SELECT * FROM OPENROWSET(
                    ''SQLNCLI'', 
                    ''Server=' + @ServerName + ';Trusted_Connection=yes;Connection Timeout=10'',
                    ''SELECT 
                        @@SERVERNAME AS ServerName,
                        CAST(SERVERPROPERTY(''''ProductVersion'''') AS NVARCHAR(50)) AS ProductVersion,
                        CAST(SERVERPROPERTY(''''ProductLevel'''') AS NVARCHAR(50)) AS ProductLevel,
                        CAST(SERVERPROPERTY(''''ProductUpdateLevel'''') AS NVARCHAR(50)) AS ProductUpdateLevel,
                        CAST(SERVERPROPERTY(''''Edition'''') AS NVARCHAR(128)) AS Edition,
                        CAST(SERVERPROPERTY(''''ProductMajorVersion'''') AS INT) AS MajorVersion''
                )';
                
                EXEC sp_executesql @SQL;
            END
            
            -- If we got data, update the main table
            IF EXISTS (SELECT 1 FROM #ServerInfo WHERE ProductVersion IS NOT NULL)
            BEGIN
                DECLARE @Version NVARCHAR(50), @Level NVARCHAR(50), @CU NVARCHAR(50), @Ed NVARCHAR(128), @Major INT;
                DECLARE @LatestCU NVARCHAR(20), @LatestBuild NVARCHAR(20), @VersionName NVARCHAR(50);
                DECLARE @MissingPatches INT = 0;
                
                SELECT @Version = ProductVersion, @Level = ProductLevel, @CU = ProductUpdateLevel, @Ed = Edition, @Major = MajorVersion
                FROM #ServerInfo;
                
                -- Get latest build info
                SELECT @LatestCU = LatestCU, @LatestBuild = LatestBuild, @VersionName = VersionName
                FROM @BuildRef WHERE MajorVersion = @Major;
                
                -- Calculate missing patches
                IF @CU LIKE 'CU%' AND @LatestCU LIKE 'CU%'
                BEGIN
                    DECLARE @CurrentCUNum INT, @LatestCUNum INT;
                    SET @CurrentCUNum = TRY_CAST(REPLACE(@CU, 'CU', '') AS INT);
                    SET @LatestCUNum = TRY_CAST(REPLACE(@LatestCU, 'CU', '') AS INT);
                    SET @MissingPatches = ISNULL(@LatestCUNum - @CurrentCUNum, 0);
                    IF @MissingPatches < 0 SET @MissingPatches = 0;
                END
                
                -- Update the record
                UPDATE Patching.TaniumPatchStatus
                SET SQLVersion = @VersionName,
                    SQLEdition = @Ed,
                    CurrentBuildNumber = @Version,
                    CurrentCULevel = ISNULL(@CU, @Level),
                    LatestAvailableCU = @LatestCU,
                    LatestAvailableBuild = @LatestBuild,
                    MissingPatches = @MissingPatches,
                    TaniumAgentStatus = 'Online',
                    TaniumLastScanDate = GETDATE(),
                    ModifiedDate = GETDATE()
                WHERE ServerName = @ServerName;
                
                SET @SuccessCount = @SuccessCount + 1;
                PRINT 'OK: ' + @ServerName + ' - ' + ISNULL(@VersionName, 'Unknown') + ' ' + ISNULL(@CU, @Level);
            END
            ELSE
            BEGIN
                -- Mark as offline/unreachable
                UPDATE Patching.TaniumPatchStatus
                SET TaniumAgentStatus = 'Offline',
                    TaniumLastScanDate = GETDATE(),
                    ModifiedDate = GETDATE()
                WHERE ServerName = @ServerName;
                
                SET @FailCount = @FailCount + 1;
                PRINT 'OFFLINE: ' + @ServerName;
            END
            
        END TRY
        BEGIN CATCH
            SET @ErrorMsg = ERROR_MESSAGE();
            
            UPDATE Patching.TaniumPatchStatus
            SET TaniumAgentStatus = 'Error',
                ComplianceNotes = LEFT('Connection error: ' + @ErrorMsg, 500),
                TaniumLastScanDate = GETDATE(),
                ModifiedDate = GETDATE()
            WHERE ServerName = @ServerName;
            
            SET @FailCount = @FailCount + 1;
            PRINT 'ERROR: ' + @ServerName + ' - ' + LEFT(@ErrorMsg, 100);
        END CATCH
        
        FETCH NEXT FROM server_cursor INTO @ServerName;
    END
    
    CLOSE server_cursor;
    DEALLOCATE server_cursor;
    
    -- Clean up
    IF OBJECT_ID('tempdb..#ServerInfo') IS NOT NULL DROP TABLE #ServerInfo;
    
    PRINT '';
    PRINT 'Collection complete: ' + CAST(@SuccessCount AS VARCHAR) + ' succeeded, ' + CAST(@FailCount AS VARCHAR) + ' failed.';
END

GO

-- [Patching].[usp_CollectTaniumTagStatus]

-- â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
-- Procedure: usp_CollectTaniumTagStatus
-- Purpose: Collect Tanium tag status from all servers using xp_regread
-- â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
CREATE PROCEDURE Patching.usp_CollectTaniumTagStatus
    @ServerName NVARCHAR(128) = NULL  -- NULL = all servers
AS
BEGIN
    /*
    â•”â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•—
    â•‘ DBAOps Monitoring and Compliance Framework                                â•‘
    â•‘ Procedure: usp_CollectTaniumTagStatus                                     â•‘
    â•‘ Author: John Bosco RHOYLLE INNOVATION EMPIRE™, Sr SQL DBA/DBAOps Engineer                   â•‘
    â•‘                                                                           â•‘
    â•‘ Collects Tanium patching tag status from remote servers via xp_regread   â•‘
    â•šâ•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
    */
    SET NOCOUNT ON;
    
    DECLARE @SQL NVARCHAR(MAX);
    DECLARE @CurrentServer NVARCHAR(128);
    DECLARE @TagFound NVARCHAR(100);
    DECLARE @IsPublisher BIT;
    DECLARE @IsSubscriber BIT;
    DECLARE @ErrorMsg NVARCHAR(500);
    
    -- Cursor through servers
    DECLARE server_cursor CURSOR FOR
        SELECT ServerName 
        FROM Patching.TaniumPatchStatus 
        WHERE IsActive = 1 
          AND TaniumAgentStatus = 'Online'
          AND (@ServerName IS NULL OR ServerName = @ServerName)
        ORDER BY ServerName;
    
    OPEN server_cursor;
    FETCH NEXT FROM server_cursor INTO @CurrentServer;
    
    WHILE @@FETCH_STATUS = 0
    BEGIN
        BEGIN TRY
            -- Build dynamic SQL to check registry on remote server
            SET @SQL = N'
            DECLARE @TagValue NVARCHAR(500);
            DECLARE @FoundTag NVARCHAR(100) = NULL;
            DECLARE @IsPublisher BIT = 0, @IsSubscriber BIT = 0;
            
            -- Check for each tag
            BEGIN TRY
                EXEC master.dbo.xp_regread ''HKEY_LOCAL_MACHINE'', 
                    ''SOFTWARE\WOW6432Node\Tanium\Tanium Client\Sensor Data\Tags'',
                    ''SERVER_SQLSERVER_PRODUCTION'', @TagValue OUTPUT;
                IF @TagValue IS NOT NULL SET @FoundTag = ''SERVER_SQLSERVER_PRODUCTION'';
            END TRY BEGIN CATCH END CATCH
            
            IF @FoundTag IS NULL
            BEGIN TRY
                SET @TagValue = NULL;
                EXEC master.dbo.xp_regread ''HKEY_LOCAL_MACHINE'', 
                    ''SOFTWARE\WOW6432Node\Tanium\Tanium Client\Sensor Data\Tags'',
                    ''SERVER_SQLSERVER_NON-PROD'', @TagValue OUTPUT;
                IF @TagValue IS NOT NULL SET @FoundTag = ''SERVER_SQLSERVER_NON-PROD'';
            END TRY BEGIN CATCH END CATCH
            
            IF @FoundTag IS NULL
            BEGIN TRY
                SET @TagValue = NULL;
                EXEC master.dbo.xp_regread ''HKEY_LOCAL_MACHINE'', 
                    ''SOFTWARE\WOW6432Node\Tanium\Tanium Client\Sensor Data\Tags'',
                    ''SERVER_SQLSERVER_PUBLISHER'', @TagValue OUTPUT;
                IF @TagValue IS NOT NULL SET @FoundTag = ''SERVER_SQLSERVER_PUBLISHER'';
            END TRY BEGIN CATCH END CATCH
            
            IF @FoundTag IS NULL
            BEGIN TRY
                SET @TagValue = NULL;
                EXEC master.dbo.xp_regread ''HKEY_LOCAL_MACHINE'', 
                    ''SOFTWARE\WOW6432Node\Tanium\Tanium Client\Sensor Data\Tags'',
                    ''SERVER_SQLSERVER_SUBSCRIBER'', @TagValue OUTPUT;
                IF @TagValue IS NOT NULL SET @FoundTag = ''SERVER_SQLSERVER_SUBSCRIBER'';
            END TRY BEGIN CATCH END CATCH
            
            -- Check replication
            IF EXISTS (SELECT 1 FROM sys.databases WHERE is_published = 1 OR is_merge_published = 1)
                SET @IsPublisher = 1;
            IF EXISTS (SELECT 1 FROM sys.databases WHERE is_subscribed = 1)
                SET @IsSubscriber = 1;
            
            SELECT @FoundTag AS TaniumTag, @IsPublisher AS IsPublisher, @IsSubscriber AS IsSubscriber;
            ';
            
            -- Execute on remote server and capture results
            DECLARE @Results TABLE (TaniumTag NVARCHAR(100), IsPublisher BIT, IsSubscriber BIT);
            DELETE FROM @Results;
            
            INSERT INTO @Results
            EXEC sp_executesql @SQL;
            
            SELECT @TagFound = TaniumTag, @IsPublisher = IsPublisher, @IsSubscriber = IsSubscriber
            FROM @Results;
            
            -- Update the status
            UPDATE Patching.TaniumPatchStatus
            SET TaniumPatchingTag = @TagFound,
                TaniumPatchingConfigured = CASE WHEN @TagFound IS NOT NULL THEN 1 ELSE 0 END,
                IsReplicationPublisher = ISNULL(@IsPublisher, 0),
                IsReplicationSubscriber = ISNULL(@IsSubscriber, 0),
                TaniumTagLastChecked = GETDATE(),
                ModifiedDate = GETDATE()
            WHERE ServerName = @CurrentServer;
            
        END TRY
        BEGIN CATCH
            SET @ErrorMsg = ERROR_MESSAGE();
            -- Log error but continue
            UPDATE Patching.TaniumPatchStatus
            SET TaniumTagLastChecked = GETDATE(),
                NonComplianceReason = ISNULL(NonComplianceReason, '') + ' Registry check failed: ' + LEFT(@ErrorMsg, 200)
            WHERE ServerName = @CurrentServer;
        END CATCH
        
        FETCH NEXT FROM server_cursor INTO @CurrentServer;
    END
    
    CLOSE server_cursor;
    DEALLOCATE server_cursor;
    
    -- Update expected tags and compliance
    EXEC Patching.usp_UpdateTaniumCompliance;
    
    PRINT 'Tanium tag collection complete.';
END

GO

-- [Patching].[usp_CompleteBatch]

-- â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
-- SECTION 15: PROCEDURE - COMPLETE BATCH
-- â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
CREATE   PROCEDURE Patching.usp_CompleteBatch
    @BatchID UNIQUEIDENTIFIER
AS
BEGIN
    SET NOCOUNT ON;
    
    UPDATE Patching.DeploymentBatches
    SET EndTime = GETDATE(),
        Status = 'Completed',
        SkippedCount = TotalServers - SuccessCount - FailedCount
    WHERE BatchID = @BatchID;
END;

GO

-- [Patching].[usp_CreatePatchRequest]

CREATE PROCEDURE Patching.usp_CreatePatchRequest
    @RequestType VARCHAR(20),
    @ServerName VARCHAR(128) = NULL,
    @ServerGroup VARCHAR(100) = NULL,
    @GroupType VARCHAR(20) = NULL,
    @ServerList VARCHAR(MAX) = NULL,
    @TargetCU VARCHAR(20) = NULL,
    @MaintenanceWindowID INT = NULL,
    @ScheduledStart DATETIME = NULL,
    @Notes VARCHAR(1000) = NULL,
    @RequestedBy VARCHAR(100) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @ServersTotal INT = 0;
    DECLARE @NewRequestID INT;
    DECLARE @KBArticle VARCHAR(20);
    
    IF @ServerName IS NOT NULL
        SET @ServersTotal = 1;
    ELSE IF @ServerList IS NOT NULL
        SET @ServersTotal = LEN(@ServerList) - LEN(REPLACE(@ServerList, ',', '')) + 1;
    ELSE IF @ServerGroup IS NOT NULL
    BEGIN
        IF @GroupType = 'Environment'
            SELECT @ServersTotal = COUNT(*) FROM config.ServerInventory WHERE Environment = @ServerGroup AND IsActive = 1;
        ELSE IF @GroupType = 'Tier'
            SELECT @ServersTotal = COUNT(*) FROM config.ServerInventory WHERE ComplianceTier = @ServerGroup AND IsActive = 1;
        ELSE IF @GroupType = 'Category'
            SELECT @ServersTotal = COUNT(*) FROM config.ServerInventory WHERE AppCategory = @ServerGroup AND IsActive = 1;
    END
    
    IF @TargetCU IS NOT NULL
        SELECT TOP 1 @KBArticle = KBArticle FROM Patching.SQLBuildReference WHERE CumulativeUpdate = @TargetCU AND IsLatest = 1;
    
    INSERT INTO Patching.PatchRequests (
        RequestType, ServerName, ServerGroup, GroupType, ServerList,
        TargetCU, KBArticle, MaintenanceWindowID, ScheduledStart,
        ServersTotal, RequestedBy, Notes
    )
    VALUES (
        @RequestType, @ServerName, @ServerGroup, @GroupType, @ServerList,
        @TargetCU, @KBArticle, @MaintenanceWindowID, @ScheduledStart,
        @ServersTotal, ISNULL(@RequestedBy, SYSTEM_USER), @Notes
    );
    
    SET @NewRequestID = SCOPE_IDENTITY();
    SELECT @NewRequestID AS RequestID, @ServersTotal AS ServersTotal;
END

GO

-- [Patching].[usp_GenerateLinkedServerScripts]

CREATE   PROCEDURE [Patching].[usp_GenerateLinkedServerScripts]
    @TargetServer NVARCHAR(128) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    -- Generate linked server creation scripts using CMS as source
    SELECT 
        cms.ServerName,
        COALESCE(cms.Environment, cms.CMSEnvironment, 'Unknown') AS Environment,
        'EXEC sp_addlinkedserver @server = N''' + cms.ServerName + ''', @srvproduct = N''SQL Server'';' AS CreateScript,
        'EXEC sp_dropserver @server = N''' + cms.ServerName + ''', @droplogins = ''droplogins'';' AS DropScript
    FROM config.ServerInventory cms
    WHERE cms.IsActive = 1
    AND (cms.ServerType = 'SQL Server' OR cms.ServerType IS NULL)
    AND (@TargetServer IS NULL OR cms.ServerName = @TargetServer)
    ORDER BY cms.Environment, cms.ServerName;
END

GO

-- [Patching].[usp_GeneratePatchReport_CSV]

CREATE   PROCEDURE Patching.usp_GeneratePatchReport_CSV
    @ReportType VARCHAR(50), -- 'Scheduled', 'History', 'Status', 'Compliance'
    @StartDate DATE = NULL,
    @EndDate DATE = NULL,
    @Environment VARCHAR(50) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    SET @StartDate = ISNULL(@StartDate, DATEADD(MONTH, -1, GETDATE()));
    SET @EndDate = ISNULL(@EndDate, GETDATE());
    
    IF @ReportType = 'Scheduled'
    BEGIN
        SELECT 
            CAST(s.ScheduleID AS VARCHAR) + ',' +
            '"' + ISNULL(s.ScheduleName, '') + '",' +
            ISNULL(s.ScheduleType, '') + ',' +
            ISNULL(s.ServerName, '') + ',' +
            ISNULL(g.GroupName, '') + ',' +
            ISNULL(s.Environment, '') + ',' +
            ISNULL(s.ComplianceTier, '') + ',' +
            ISNULL(s.PatchType, '') + ',' +
            ISNULL(s.TargetCU, '') + ',' +
            ISNULL(s.KBNumber, '') + ',' +
            CONVERT(VARCHAR, s.ScheduledDate, 120) + ',' +
            CONVERT(VARCHAR, s.ScheduledStartTime, 108) + ',' +
            CONVERT(VARCHAR, s.ScheduledEndTime, 108) + ',' +
            ISNULL(s.Status, '') + ',' +
            ISNULL(s.ApprovalStatus, '') + ',' +
            ISNULL(s.ApprovedBy, '') + ',' +
            ISNULL(s.ChangeTicketNumber, '') + ',' +
            ISNULL(s.CreatedBy, '') AS CSVRow
        FROM Patching.ScheduledPatches s
        LEFT JOIN Patching.ServerPatchGroups g ON s.GroupID = g.GroupID
        WHERE s.ScheduledDate BETWEEN @StartDate AND @EndDate
        AND (@Environment IS NULL OR s.Environment = @Environment)
        ORDER BY s.ScheduledDate;
    END
    ELSE IF @ReportType = 'History'
    BEGIN
        SELECT 
            CAST(h.ExecutionID AS VARCHAR) + ',' +
            ISNULL(h.ServerName, '') + ',' +
            ISNULL(cms.Environment, '') + ',' +
            ISNULL(cms.ComplianceTier, '') + ',' +
            ISNULL(h.PatchType, '') + ',' +
            ISNULL(h.KBNumber, '') + ',' +
            ISNULL(h.BeforeBuild, '') + ',' +
            ISNULL(h.AfterBuild, '') + ',' +
            CONVERT(VARCHAR, h.StartTime, 120) + ',' +
            ISNULL(CONVERT(VARCHAR, h.EndTime, 120), '') + ',' +
            ISNULL(CAST(h.DurationMinutes AS VARCHAR), '') + ',' +
            ISNULL(h.Status, '') + ',' +
            ISNULL(h.ExecutedBy, '') AS CSVRow
        FROM Patching.PatchExecutionHistory h
        LEFT JOIN config.ServerInventory cms ON h.ServerName = cms.ServerName
        WHERE h.StartTime BETWEEN @StartDate AND @EndDate
        AND (@Environment IS NULL OR cms.Environment = @Environment)
        ORDER BY h.StartTime DESC;
    END
    ELSE IF @ReportType = 'Status'
    BEGIN
        SELECT 
            cms.ServerName + ',' +
            ISNULL(COALESCE(cms.Environment, cms.CMSEnvironment), '') + ',' +
            ISNULL(cms.ComplianceTier, '') + ',' +
            ISNULL(cms.SQLVersion, '') + ',' +
            ISNULL(cms.SqlCumulativeUpdate, '') + ',' +
            ISNULL(br.LatestCU, '') + ',' +
            ISNULL(kb.KBNumber, '') + ',' +
            CASE WHEN cms.SqlCumulativeUpdate = br.LatestCU THEN 'Current' 
                 WHEN cms.SqlCumulativeUpdate IS NULL THEN 'Unknown' 
                 ELSE 'Needs Patch' END + ',' +
            CASE WHEN kb.IsSecurityUpdate = 1 THEN 'Yes' ELSE 'No' END + ',' +
            ISNULL(g.GroupName, '') AS CSVRow
        FROM config.ServerInventory cms
        LEFT JOIN Patching.SQLServerBuildReference br ON cms.SQLVersion LIKE CAST(br.SQLMajorVersion AS VARCHAR) + '.%'
        LEFT JOIN Patching.MicrosoftKBReference kb ON br.SQLMajorVersion = kb.SQLMajorVersion AND kb.IsLatest = 1
        LEFT JOIN Patching.ServerGroupMembers sgm ON cms.ServerName = sgm.ServerName AND sgm.IsActive = 1
        LEFT JOIN Patching.ServerPatchGroups g ON sgm.GroupID = g.GroupID
        WHERE cms.IsActive = 1 AND (cms.ServerType = 'SQL Server' OR cms.ServerType IS NULL)
        AND (@Environment IS NULL OR COALESCE(cms.Environment, cms.CMSEnvironment) = @Environment)
        ORDER BY cms.ServerName;
    END
END

GO

-- [Patching].[usp_GenerateTaniumComplianceReport]

CREATE PROCEDURE Patching.usp_GenerateTaniumComplianceReport
    @SendEmail BIT = 1,
    @TestMode BIT = 0,
    @TestEmailAddress NVARCHAR(256) = NULL
AS
BEGIN
    /*
    â•”â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•—
    â•‘ DBAOps Monitoring and Compliance Framework                                â•‘
    â•‘ Procedure: usp_GenerateTaniumComplianceReport                             â•‘
    â•‘ Author: John Bosco RHOYLLE INNOVATION EMPIRE™, Sr SQL DBA/DBAOps Engineer                   â•‘
    â•‘ Standards: CJIS 5.10.4.1 / NIST SP 800-53 SI-2 / NIST SP 800-40          â•‘
    â•šâ•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
    */
    SET NOCOUNT ON;
    
    DECLARE @StartTime DATETIME = GETDATE();
    DECLARE @EmailRecipients NVARCHAR(MAX);
    DECLARE @EmailSubject NVARCHAR(256);
    DECLARE @EmailBody NVARCHAR(MAX);
    DECLARE @MailProfile NVARCHAR(128) = 'DBAOpsProfile';
    
    -- Statistics
    DECLARE @TotalServers INT = 0;
    DECLARE @FullyCompliant INT = 0;
    DECLARE @PatchCompliant INT = 0;
    DECLARE @TaniumConfigured INT = 0;
    DECLARE @TaniumNotConfigured INT = 0;
    DECLARE @PatchBehind INT = 0;
    DECLARE @OfflineServers INT = 0;
    DECLARE @TagMismatch INT = 0;
    DECLARE @ExcludedServers INT = 0;
    DECLARE @OverallCompliancePct DECIMAL(5,2) = 0;
    
    DECLARE @ReportStatus NVARCHAR(50) = 'Success';
    DECLARE @ErrorMessage NVARCHAR(MAX) = NULL;
    DECLARE @FrameworkName NVARCHAR(100);
    DECLARE @FrameworkAuthor NVARCHAR(200);
    
    BEGIN TRY
        -- Get configuration
        SELECT @EmailRecipients = ConfigValue FROM Patching.ComplianceConfiguration WHERE ConfigKey = 'EmailRecipients';
        SELECT @MailProfile = ISNULL(ConfigValue, 'DBAOpsProfile') FROM Patching.ComplianceConfiguration WHERE ConfigKey = 'MailProfile';
        SELECT @FrameworkName = ConfigValue FROM Patching.ComplianceConfiguration WHERE ConfigKey = 'FrameworkName';
        SELECT @FrameworkAuthor = ConfigValue FROM Patching.ComplianceConfiguration WHERE ConfigKey = 'FrameworkAuthor';
        
        IF @TestMode = 1 AND @TestEmailAddress IS NOT NULL
            SET @EmailRecipients = @TestEmailAddress;
        
        -- Calculate statistics (excluding servers marked for exclusion from Tanium automation counts)
        SELECT 
            @TotalServers = COUNT(*),
            @FullyCompliant = SUM(CASE WHEN ComplianceStatus = 'Compliant' AND (TaniumPatchingConfigured = 1 OR ExcludeFromTaniumAutomation = 1) AND ISNULL(TaniumTagMismatch,0) = 0 THEN 1 ELSE 0 END),
            @PatchCompliant = SUM(CASE WHEN ComplianceStatus = 'Compliant' THEN 1 ELSE 0 END),
            @TaniumConfigured = SUM(CASE WHEN TaniumPatchingConfigured = 1 OR ExcludeFromTaniumAutomation = 1 THEN 1 ELSE 0 END),
            @TaniumNotConfigured = SUM(CASE WHEN (TaniumPatchingConfigured = 0 OR TaniumPatchingConfigured IS NULL) AND ISNULL(ExcludeFromTaniumAutomation,0) = 0 AND TaniumAgentStatus != 'Offline' THEN 1 ELSE 0 END),
            @PatchBehind = SUM(CASE WHEN ComplianceStatus IN ('Warning', 'Non-Compliant') AND TaniumAgentStatus != 'Offline' THEN 1 ELSE 0 END),
            @OfflineServers = SUM(CASE WHEN TaniumAgentStatus = 'Offline' THEN 1 ELSE 0 END),
            @TagMismatch = SUM(CASE WHEN TaniumTagMismatch = 1 AND ISNULL(ExcludeFromTaniumAutomation,0) = 0 THEN 1 ELSE 0 END),
            @ExcludedServers = SUM(CASE WHEN ExcludeFromTaniumAutomation = 1 THEN 1 ELSE 0 END)
        FROM Patching.TaniumPatchStatus 
        WHERE IsActive = 1;
        
        -- Calculate overall compliance (excluding offline)
        DECLARE @OnlineServers INT = @TotalServers - @OfflineServers;
        SET @OverallCompliancePct = CASE WHEN @OnlineServers > 0 THEN CAST(@FullyCompliant AS DECIMAL) / @OnlineServers * 100 ELSE 0 END;
        
        -- Determine email importance and status
        DECLARE @EmailImportance VARCHAR(10) = 'Normal';
        DECLARE @OverallStatus NVARCHAR(50);
        
        IF @TaniumNotConfigured > 0 OR @PatchBehind > 5
        BEGIN
            SET @EmailImportance = 'High';
            SET @OverallStatus = 'ACTION REQUIRED';
        END
        ELSE IF @PatchBehind > 0 OR @TagMismatch > 0
        BEGIN
            SET @EmailImportance = 'Normal';
            SET @OverallStatus = 'WARNING';
        END
        ELSE
        BEGIN
            SET @EmailImportance = 'Low';
            SET @OverallStatus = 'COMPLIANT';
        END
        
        -- Build subject
        SET @EmailSubject = 'Weekly Tanium SQL Patching Compliance Report - ' + CONVERT(VARCHAR(10), GETDATE(), 120) + ' - ' + @OverallStatus;
        
        -- Build HTML body
        SET @EmailBody = N'<!DOCTYPE html><html><head><style>
body{font-family:Segoe UI,Arial,sans-serif;margin:0;padding:20px;background:#f5f7fa}
.container{max-width:1200px;margin:0 auto;background:#fff;border-radius:12px;box-shadow:0 4px 20px rgba(0,0,0,.08);overflow:hidden}
.header{background:linear-gradient(135deg,#1a365d 0%,#2c5282 50%,#2b6cb0 100%);color:#fff;padding:35px;text-align:center}
.header h1{margin:0 0 8px 0;font-size:26px;font-weight:600;letter-spacing:0.5px}
.header .subtitle{opacity:.9;font-size:14px;margin-bottom:15px}
.header .author{font-size:12px;opacity:.75;border-top:1px solid rgba(255,255,255,.2);padding-top:15px;margin-top:10px}
.content{padding:30px}
h2{color:#1a365d;margin:30px 0 15px 0;padding-bottom:10px;border-bottom:3px solid #e2e8f0;font-size:18px}
.summary-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:15px;margin:25px 0}
.metric{padding:20px;border-radius:10px;text-align:center;transition:transform .2s}
.metric:hover{transform:translateY(-2px)}
.metric-value{font-size:2.5em;font-weight:700;line-height:1}
.metric-label{font-size:.8em;margin-top:8px;opacity:.85;font-weight:500}
.m-total{background:linear-gradient(135deg,#e0e7ff,#c7d2fe);color:#3730a3}
.m-compliant{background:linear-gradient(135deg,#d1fae5,#a7f3d0);color:#065f46}
.m-configured{background:linear-gradient(135deg,#dbeafe,#bfdbfe);color:#1e40af}
.m-notconfig{background:linear-gradient(135deg,#fef3c7,#fde68a);color:#92400e}
.m-behind{background:linear-gradient(135deg,#fed7aa,#fdba74);color:#9a3412}
.m-offline{background:linear-gradient(135deg,#e5e7eb,#d1d5db);color:#374151}
.m-excluded{background:linear-gradient(135deg,#fce7f3,#fbcfe8);color:#9d174d}
.status-badge{display:inline-block;padding:12px 30px;border-radius:30px;font-weight:700;font-size:1.1em;color:#fff;margin:20px 0;text-transform:uppercase;letter-spacing:1px}
.badge-compliant{background:linear-gradient(135deg,#059669,#10b981)}
.badge-warning{background:linear-gradient(135deg,#d97706,#f59e0b)}
.badge-action{background:linear-gradient(135deg,#dc2626,#ef4444)}
table{width:100%;border-collapse:separate;border-spacing:0;margin:20px 0;font-size:.9em;border-radius:8px;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,.1)}
th{background:linear-gradient(135deg,#1a365d,#2c5282);color:#fff;font-weight:600;padding:14px 12px;text-align:left}
td{padding:12px;border-bottom:1px solid #e2e8f0}
tr:hover{background:#f8fafc}
tr:last-child td{border-bottom:none}
.status-ok{color:#059669;font-weight:600}
.status-warning{color:#d97706;font-weight:600}
.status-error{color:#dc2626;font-weight:600}
.status-offline{color:#6b7280;font-weight:600}
.status-excluded{color:#9d174d;font-weight:600}
.solution-box{background:#f0fdf4;border-left:4px solid #22c55e;padding:8px 12px;font-size:.85em;border-radius:0 4px 4px 0}
.problem-box{background:#fef2f2;border-left:4px solid #ef4444;padding:8px 12px;font-size:.85em;border-radius:0 4px 4px 0}
.excluded-box{background:#fdf2f8;border-left:4px solid #ec4899;padding:8px 12px;font-size:.85em;border-radius:0 4px 4px 0}
.info-box{background:#eff6ff;border-left:4px solid #3b82f6;padding:15px;border-radius:0 8px 8px 0;margin:15px 0}
.standards-box{background:#fffbeb;border-left:4px solid #f59e0b;padding:15px;border-radius:0 8px 8px 0;margin:20px 0}
.footer{background:#f8fafc;padding:25px 30px;border-top:1px solid #e2e8f0;font-size:.85em;color:#64748b}
.tag-code{font-family:Consolas,monospace;background:#f1f5f9;padding:2px 6px;border-radius:4px;font-size:.85em}
.env-prod{background:#dbeafe;color:#1e40af;padding:3px 8px;border-radius:4px;font-size:.8em;font-weight:600}
.env-nonprod{background:#fef3c7;color:#92400e;padding:3px 8px;border-radius:4px;font-size:.8em;font-weight:600}
</style></head><body><div class="container">
<div class="header">
<h1>ðŸ”’ Weekly Tanium SQL Patching Compliance Report</h1>
<div class="subtitle">CJIS Security Policy 5.10.4.1 | NIST SP 800-53 SI-2 | NIST SP 800-40</div>
<div class="author">
<strong>' + ISNULL(@FrameworkName, 'DBAOps Monitoring and Compliance Framework') + '</strong><br/>
' + ISNULL(@FrameworkAuthor, 'DBA Team') + '
</div>
</div>
<div class="content">

<div class="info-box">
<strong>ðŸ“… Report Date:</strong> ' + FORMAT(GETDATE(), 'dddd, MMMM dd, yyyy HH:mm') + '<br/>
<strong>ðŸ“Š Total SQL Servers Monitored:</strong> ' + CAST(@TotalServers AS VARCHAR) + ' (' + CAST(@OnlineServers AS VARCHAR) + ' online, ' + CAST(@OfflineServers AS VARCHAR) + ' offline, ' + CAST(@ExcludedServers AS VARCHAR) + ' excluded)<br/>
<strong>âœ… Overall Compliance Rate:</strong> ' + CAST(CAST(@OverallCompliancePct AS DECIMAL(5,1)) AS VARCHAR) + '% of online servers fully compliant
</div>

<h2>ðŸ“ˆ Executive Summary</h2>
<div class="summary-grid">
<div class="metric m-total"><div class="metric-value">' + CAST(@TotalServers AS VARCHAR) + '</div><div class="metric-label">Total Servers</div></div>
<div class="metric m-compliant"><div class="metric-value">' + CAST(@FullyCompliant AS VARCHAR) + '</div><div class="metric-label">Fully Compliant</div></div>
<div class="metric m-configured"><div class="metric-value">' + CAST(@TaniumConfigured AS VARCHAR) + '</div><div class="metric-label">Tanium Configured</div></div>
<div class="metric m-notconfig"><div class="metric-value">' + CAST(@TaniumNotConfigured AS VARCHAR) + '</div><div class="metric-label">Tanium Missing</div></div>
<div class="metric m-behind"><div class="metric-value">' + CAST(@PatchBehind AS VARCHAR) + '</div><div class="metric-label">Patches Behind</div></div>
<div class="metric m-excluded"><div class="metric-value">' + CAST(@ExcludedServers AS VARCHAR) + '</div><div class="metric-label">Excluded (Manual)</div></div>
<div class="metric m-offline"><div class="metric-value">' + CAST(@OfflineServers AS VARCHAR) + '</div><div class="metric-label">Offline</div></div>
</div>

<div style="text-align:center">
<span class="status-badge ' + 
    CASE @OverallStatus 
        WHEN 'COMPLIANT' THEN 'badge-compliant">âœ“ ALL SYSTEMS COMPLIANT'
        WHEN 'WARNING' THEN 'badge-warning">âš  ATTENTION NEEDED'
        ELSE 'badge-action">ðŸš¨ ACTION REQUIRED'
    END + '</span>
</div>

<div class="standards-box">
<strong>ðŸ“‹ Tanium Patching Automation Tags:</strong><br/>
<table style="margin:10px 0;box-shadow:none">
<tr><td><span class="tag-code">SERVER_SQLSERVER_PRODUCTION</span></td><td>Code 4</td><td>Production servers - Prod maintenance window</td></tr>
<tr><td><span class="tag-code">SERVER_SQLSERVER_NON-PROD</span></td><td>Code 3</td><td>Non-Production servers - Non-Prod maintenance window</td></tr>
<tr><td><span class="tag-code">SERVER_SQLSERVER_PUBLISHER</span></td><td>Code 2</td><td>Replication Publishers - Prod maintenance window</td></tr>
<tr><td><span class="tag-code">SERVER_SQLSERVER_SUBSCRIBER</span></td><td>Code 1</td><td>Replication Subscribers - Prod maintenance window</td></tr>
<tr><td><span class="tag-code">EXCLUDED-MANUAL</span></td><td>N/A</td><td>Critical systems - Manual patching only</td></tr>
</table>
</div>';

        -- SECTION: Excluded Critical Servers
        IF @ExcludedServers > 0
        BEGIN
            SET @EmailBody = @EmailBody + N'
<h2 style="color:#9d174d">ðŸ” Excluded Critical Servers (' + CAST(@ExcludedServers AS VARCHAR) + ')</h2>
<p>These servers are excluded from Tanium automation and are manually patched due to their critical nature.</p>
<table>
<tr><th>Server</th><th>SQL Version</th><th>Current CU</th><th>Patch Status</th><th>Exclusion Reason</th></tr>';
            
            SELECT @EmailBody = @EmailBody + N'
<tr>
<td><strong>' + ServerName + '</strong></td>
<td>' + ISNULL(SQLVersion,'N/A') + '</td>
<td>' + ISNULL(CurrentCULevel,'N/A') + '</td>
<td>' + CASE WHEN ComplianceStatus = 'Compliant' THEN '<span class="status-ok">âœ“ Current</span>' ELSE '<span class="status-warning">' + CAST(ISNULL(MissingPatches,0) AS VARCHAR) + ' CU(s) behind</span>' END + '</td>
<td><div class="excluded-box">' + ISNULL(ExclusionReason,'Manual patching required') + '</div></td>
</tr>'
            FROM Patching.TaniumPatchStatus 
            WHERE IsActive = 1 AND ExcludeFromTaniumAutomation = 1
            ORDER BY ServerName;
            
            SET @EmailBody = @EmailBody + N'</table>';
        END

        -- SECTION: Servers Missing Tanium Configuration
        IF @TaniumNotConfigured > 0
        BEGIN
            SET @EmailBody = @EmailBody + N'
<h2 style="color:#dc2626">ðŸš¨ Servers Missing Tanium Automation (' + CAST(@TaniumNotConfigured AS VARCHAR) + ')</h2>
<p>These servers do not have Tanium patching automation configured. Patches will NOT be applied automatically.</p>
<table>
<tr><th>Server</th><th>Environment</th><th>SQL Version</th><th>Required Tag</th><th>Setup Code</th><th>Solution</th></tr>';
            
            SELECT @EmailBody = @EmailBody + N'
<tr>
<td><strong>' + ServerName + '</strong></td>
<td><span class="' + CASE WHEN ServerEnvironment = 'Production' THEN 'env-prod' ELSE 'env-nonprod' END + '">' + ISNULL(ServerEnvironment,'Unknown') + '</span></td>
<td>' + ISNULL(SQLVersion,'N/A') + '</td>
<td><span class="tag-code">' + ISNULL(ExpectedTaniumTag,'N/A') + '</span></td>
<td><strong>' + CASE WHEN ServerEnvironment = 'Non-Prod' THEN '3' ELSE '4' END + '</strong></td>
<td><div class="solution-box">' + ISNULL(ProposedSolution,'Run Tanium tagging script') + '</div></td>
</tr>'
            FROM Patching.TaniumPatchStatus 
            WHERE IsActive = 1 
              AND (TaniumPatchingConfigured = 0 OR TaniumPatchingConfigured IS NULL)
              AND ISNULL(ExcludeFromTaniumAutomation, 0) = 0
              AND TaniumAgentStatus != 'Offline'
            ORDER BY ServerEnvironment DESC, ServerName;
            
            SET @EmailBody = @EmailBody + N'</table>';
        END

        -- SECTION: Servers Behind on Patches
        IF @PatchBehind > 0
        BEGIN
            SET @EmailBody = @EmailBody + N'
<h2 style="color:#d97706">âš  Servers Behind on Patches (' + CAST(@PatchBehind AS VARCHAR) + ')</h2>
<p>These servers have pending cumulative updates that need to be installed.</p>
<table>
<tr><th>Server</th><th>Environment</th><th>Current CU</th><th>Latest CU</th><th>Behind</th><th>Tanium Tag</th><th>Issue & Solution</th></tr>';
            
            SELECT @EmailBody = @EmailBody + N'
<tr>
<td><strong>' + ServerName + '</strong></td>
<td><span class="' + CASE WHEN ServerEnvironment = 'Production' THEN 'env-prod' ELSE 'env-nonprod' END + '">' + ISNULL(ServerEnvironment,'Unknown') + '</span></td>
<td class="status-warning">' + ISNULL(CurrentCULevel,'RTM') + '</td>
<td class="status-ok">' + ISNULL(LatestAvailableCU,'N/A') + '</td>
<td class="status-error"><strong>' + CAST(ISNULL(MissingPatches,0) AS VARCHAR) + ' CU(s)</strong></td>
<td>' + CASE WHEN TaniumPatchingConfigured = 1 THEN '<span class="status-ok">âœ“ ' + ISNULL(TaniumPatchingTag,'Set') + '</span>' 
              WHEN ExcludeFromTaniumAutomation = 1 THEN '<span class="status-excluded">Manual</span>'
              ELSE '<span class="status-error">âœ— Not Set</span>' END + '</td>
<td>
<div class="problem-box">' + ISNULL(NonComplianceReason,'Behind on patches') + '</div>
<div class="solution-box">' + ISNULL(ProposedSolution,'Apply latest CU during maintenance window') + '</div>
</td>
</tr>'
            FROM Patching.TaniumPatchStatus 
            WHERE IsActive = 1 
              AND ComplianceStatus IN ('Warning', 'Non-Compliant')
              AND TaniumAgentStatus != 'Offline'
            ORDER BY MissingPatches DESC, ServerName;
            
            SET @EmailBody = @EmailBody + N'</table>';
        END

        -- SECTION: Tag Mismatches
        IF @TagMismatch > 0
        BEGIN
            SET @EmailBody = @EmailBody + N'
<h2 style="color:#7c3aed">ðŸ”„ Tanium Tag Mismatches (' + CAST(@TagMismatch AS VARCHAR) + ')</h2>
<p>These servers have a Tanium tag that does not match their expected configuration.</p>
<table>
<tr><th>Server</th><th>Environment</th><th>Current Tag</th><th>Expected Tag</th><th>Solution</th></tr>';
            
            SELECT @EmailBody = @EmailBody + N'
<tr>
<td><strong>' + ServerName + '</strong></td>
<td><span class="' + CASE WHEN ServerEnvironment = 'Production' THEN 'env-prod' ELSE 'env-nonprod' END + '">' + ISNULL(ServerEnvironment,'Unknown') + '</span></td>
<td class="status-warning"><span class="tag-code">' + ISNULL(TaniumPatchingTag,'None') + '</span></td>
<td class="status-ok"><span class="tag-code">' + ISNULL(ExpectedTaniumTag,'N/A') + '</span></td>
<td><div class="solution-box">' + ISNULL(ProposedSolution,'Update Tanium tag') + '</div></td>
</tr>'
            FROM Patching.TaniumPatchStatus 
            WHERE IsActive = 1 AND TaniumTagMismatch = 1 AND ISNULL(ExcludeFromTaniumAutomation,0) = 0
            ORDER BY ServerName;
            
            SET @EmailBody = @EmailBody + N'</table>';
        END

        -- SECTION: Offline Servers
        IF @OfflineServers > 0
        BEGIN
            SET @EmailBody = @EmailBody + N'
<h2 style="color:#6b7280">ðŸ“´ Offline/Unreachable Servers (' + CAST(@OfflineServers AS VARCHAR) + ')</h2>
<p>These servers could not be contacted. Compliance status cannot be verified.</p>
<table>
<tr><th>Server</th><th>Last Known Version</th><th>Last Scan</th><th>Action Required</th></tr>';
            
            SELECT TOP 20 @EmailBody = @EmailBody + N'
<tr>
<td class="status-offline">' + ServerName + '</td>
<td>' + ISNULL(SQLVersion,'Unknown') + '</td>
<td>' + ISNULL(FORMAT(TaniumLastScanDate, 'yyyy-MM-dd HH:mm'),'Never') + '</td>
<td><div class="solution-box">Verify server status and network connectivity</div></td>
</tr>'
            FROM Patching.TaniumPatchStatus 
            WHERE IsActive = 1 AND TaniumAgentStatus = 'Offline'
            ORDER BY ServerName;
            
            IF @OfflineServers > 20
                SET @EmailBody = @EmailBody + N'<tr><td colspan="4"><em>...and ' + CAST(@OfflineServers - 20 AS VARCHAR) + ' more offline servers</em></td></tr>';
            
            SET @EmailBody = @EmailBody + N'</table>';
        END

        -- SECTION: Fully Compliant Servers (Summary by Version)
        SET @EmailBody = @EmailBody + N'
<h2 style="color:#059669">âœ… Fully Compliant Servers (' + CAST(@FullyCompliant AS VARCHAR) + ')</h2>';
        
        IF @FullyCompliant > 0
        BEGIN
            SET @EmailBody = @EmailBody + N'
<p>Servers that are patched AND have Tanium automation configured (or are excluded critical systems).</p>
<table>
<tr><th>SQL Version</th><th>Environment</th><th>Server Count</th><th>Status</th></tr>';
            
            SELECT @EmailBody = @EmailBody + N'
<tr>
<td>' + ISNULL(SQLVersion,'Unknown') + '</td>
<td><span class="' + CASE WHEN ServerEnvironment = 'Production' THEN 'env-prod' ELSE 'env-nonprod' END + '">' + ISNULL(ServerEnvironment,'') + '</span></td>
<td><strong>' + CAST(COUNT(*) AS VARCHAR) + '</strong></td>
<td class="status-ok">âœ“ Fully Compliant</td>
</tr>'
            FROM Patching.TaniumPatchStatus 
            WHERE IsActive = 1 
              AND ComplianceStatus = 'Compliant' 
              AND (TaniumPatchingConfigured = 1 OR ExcludeFromTaniumAutomation = 1)
              AND ISNULL(TaniumTagMismatch,0) = 0
            GROUP BY SQLVersion, ServerEnvironment
            ORDER BY ServerEnvironment DESC, SQLVersion;
            
            SET @EmailBody = @EmailBody + N'</table>';
        END
        ELSE
            SET @EmailBody = @EmailBody + N'<p>No servers are currently fully compliant.</p>';

        -- SECTION: Compliance by Environment Summary
        SET @EmailBody = @EmailBody + N'
<h2>ðŸ“Š Compliance by Environment</h2>
<table>
<tr><th>Environment</th><th>Total</th><th>Fully Compliant</th><th>Tanium Configured</th><th>Patches Current</th><th>Excluded</th><th>Compliance %</th></tr>';
        
        ;WITH EnvStats AS (
            SELECT 
                ServerEnvironment,
                COUNT(*) AS Total,
                SUM(CASE WHEN ComplianceStatus = 'Compliant' AND (TaniumPatchingConfigured = 1 OR ExcludeFromTaniumAutomation = 1) THEN 1 ELSE 0 END) AS FullyCompliant,
                SUM(CASE WHEN TaniumPatchingConfigured = 1 OR ExcludeFromTaniumAutomation = 1 THEN 1 ELSE 0 END) AS TaniumConfigured,
                SUM(CASE WHEN ComplianceStatus = 'Compliant' THEN 1 ELSE 0 END) AS PatchCurrent,
                SUM(CASE WHEN ExcludeFromTaniumAutomation = 1 THEN 1 ELSE 0 END) AS Excluded
            FROM Patching.TaniumPatchStatus 
            WHERE IsActive = 1 AND TaniumAgentStatus != 'Offline'
            GROUP BY ServerEnvironment
        )
        SELECT @EmailBody = @EmailBody + N'
<tr>
<td><span class="' + CASE WHEN ServerEnvironment = 'Production' THEN 'env-prod' ELSE 'env-nonprod' END + '">' + ServerEnvironment + '</span></td>
<td>' + CAST(Total AS VARCHAR) + '</td>
<td>' + CAST(FullyCompliant AS VARCHAR) + '</td>
<td>' + CAST(TaniumConfigured AS VARCHAR) + '</td>
<td>' + CAST(PatchCurrent AS VARCHAR) + '</td>
<td>' + CAST(Excluded AS VARCHAR) + '</td>
<td><strong>' + CAST(CAST(CASE WHEN Total > 0 THEN FullyCompliant * 100.0 / Total ELSE 0 END AS DECIMAL(5,1)) AS VARCHAR) + '%</strong></td>
</tr>'
        FROM EnvStats
        ORDER BY ServerEnvironment DESC;
        
        SET @EmailBody = @EmailBody + N'</table>';

        -- Footer
        SET @EmailBody = @EmailBody + N'
</div>
<div class="footer">
<strong>ðŸ“§ Report Generated:</strong> ' + FORMAT(GETDATE(), 'yyyy-MM-dd HH:mm:ss') + '<br/>
<strong>ðŸ¢ Framework:</strong> ' + ISNULL(@FrameworkName, 'DBAOps Monitoring and Compliance Framework') + '<br/>
<strong>ðŸ‘¤ Author:</strong> ' + ISNULL(@FrameworkAuthor, 'DBA Team') + '<br/>
<strong>ðŸ“¬ Recipients:</strong> ' + @EmailRecipients + '<br/><br/>
<em>This is an automated weekly compliance report. For questions or to report issues, contact the DBA Team.</em><br/>
<em>Next report scheduled for: ' + FORMAT(DATEADD(WEEK, 1, GETDATE()), 'dddd, MMMM dd, yyyy') + '</em>
</div>
</div></body></html>';
        
        -- Send or display
        IF @SendEmail = 1
        BEGIN
            EXEC msdb.dbo.sp_send_dbmail
                @profile_name = @MailProfile,
                @recipients = @EmailRecipients,
                @subject = @EmailSubject,
                @body = @EmailBody,
                @body_format = 'HTML',
                @importance = @EmailImportance;
            PRINT 'Email sent successfully using profile: ' + @MailProfile;
            PRINT 'Recipients: ' + @EmailRecipients;
        END
        ELSE
            SELECT @EmailBody AS HTMLReport;
        
    END TRY
    BEGIN CATCH
        SET @ReportStatus = 'Failed';
        SET @ErrorMessage = ERROR_MESSAGE();
        PRINT 'Error: ' + @ErrorMessage;
    END CATCH
    
    -- Log report
    INSERT INTO Patching.TaniumReportLog 
    (TotalServers, FullyCompliant, PatchCompliant, TaniumConfigured, TaniumNotConfigured, PatchBehind, OfflineServers, OverallCompliancePct, ReportSentTo, ReportStatus, ErrorMessage)
    VALUES 
    (@TotalServers, @FullyCompliant, @PatchCompliant, @TaniumConfigured, @TaniumNotConfigured, @PatchBehind, @OfflineServers, @OverallCompliancePct, @EmailRecipients, @ReportStatus, @ErrorMessage);
END

GO

-- [Patching].[usp_GenerateWeeklyComplianceReport]

-- Add @OnlyOnIssues parameter to the procedure signature
-- This is a large procedure, so we'll just add the smart logic at the top

CREATE PROCEDURE Patching.usp_GenerateWeeklyComplianceReport
    @SendEmail BIT = 1,
    @TestMode BIT = 0,
    @TestEmailAddress NVARCHAR(256) = NULL,
    @OnlyOnIssues BIT = 1  -- NEW: Only send if non-compliant/warning
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @StartTime DATETIME = GETDATE();
    DECLARE @TotalServers INT = 0;
    DECLARE @CompliantCount INT = 0;
    DECLARE @NonCompliantCount INT = 0;
    DECLARE @WarningCount INT = 0;
    DECLARE @UnknownCount INT = 0;
    
    BEGIN TRY
        -- Update compliance status first
        EXEC Patching.usp_UpdateComplianceStatus;
        
        -- Calculate statistics
        SELECT 
            @TotalServers = COUNT(*),
            @CompliantCount = SUM(CASE WHEN ComplianceStatus = 'Compliant' THEN 1 ELSE 0 END),
            @NonCompliantCount = SUM(CASE WHEN ComplianceStatus = 'Non-Compliant' THEN 1 ELSE 0 END),
            @WarningCount = SUM(CASE WHEN ComplianceStatus = 'Warning' THEN 1 ELSE 0 END),
            @UnknownCount = SUM(CASE WHEN ComplianceStatus = 'Unknown' THEN 1 ELSE 0 END)
        FROM Patching.TaniumPatchStatus WHERE IsActive = 1;
        
        -- =====================================================
        -- SMART EMAIL LOGIC: Only send if issues exist
        -- =====================================================
        IF @OnlyOnIssues = 1 AND @NonCompliantCount = 0 AND @WarningCount = 0
        BEGIN
            PRINT 'DBAOps | JB: All ' + CAST(@CompliantCount AS VARCHAR(10)) + 
                ' servers compliant. Email not sent.';
            
            -- Still log the report
            INSERT INTO Patching.ComplianceReportLog (TotalServers, CompliantServers, NonCompliantServers, WarningServers, UnknownServers, CompliancePercentage, ReportSentTo, ReportStatus)
            VALUES (@TotalServers, @CompliantCount, @NonCompliantCount, @WarningCount, @UnknownCount, 
                CASE WHEN @TotalServers > 0 THEN CAST(@CompliantCount * 100.0 / @TotalServers AS DECIMAL(5,2)) ELSE 0 END,
                'NOT SENT - All Compliant', 'Skipped');
            RETURN;
        END;
        
        -- Rest of the procedure continues with email generation...
        -- (Original logic preserved for when issues exist)
        
        DECLARE @EmailRecipients NVARCHAR(MAX);
        DECLARE @MailProfile NVARCHAR(128);
        DECLARE @EmailSubject NVARCHAR(256);
        DECLARE @EmailBody NVARCHAR(MAX);
        DECLARE @CompliancePercentage DECIMAL(5,2);
        
        SELECT @EmailRecipients = ConfigValue FROM Patching.ComplianceConfiguration WHERE ConfigKey = 'EmailRecipients';
        SELECT @MailProfile = ConfigValue FROM Patching.ComplianceConfiguration WHERE ConfigKey = 'MailProfile';
        
        IF @MailProfile IS NULL SET @MailProfile = 'DBAOpsProfile';
        IF @TestMode = 1 AND @TestEmailAddress IS NOT NULL SET @EmailRecipients = @TestEmailAddress;
        
        SET @CompliancePercentage = CASE WHEN @TotalServers > 0 THEN CAST(@CompliantCount * 100.0 / @TotalServers AS DECIMAL(5,2)) ELSE 0 END;
        
        SET @EmailSubject = 'SQL Server Patching Compliance - ' + CONVERT(VARCHAR(10), GETDATE(), 120) + ' - ' +
            CASE 
                WHEN @NonCompliantCount > 0 THEN 'NON-COMPLIANT (' + CAST(@NonCompliantCount AS VARCHAR) + ')'
                WHEN @WarningCount > 0 THEN 'WARNING (' + CAST(@WarningCount AS VARCHAR) + ')'
                ELSE 'COMPLIANT'
            END;
        
        -- Build simplified HTML body
        SET @EmailBody = N'<html><body style="font-family:Segoe UI,Arial;">
        <h2>SQL Server Patching Compliance Report</h2>
        <p><strong>This report only sent when non-compliant or warning servers exist.</strong></p>
        <table border="1" cellpadding="8">
        <tr><td>Total Servers</td><td>' + CAST(@TotalServers AS VARCHAR) + N'</td></tr>
        <tr><td>Compliant</td><td style="color:green;">' + CAST(@CompliantCount AS VARCHAR) + N'</td></tr>
        <tr><td>Warning</td><td style="color:orange;font-weight:bold;">' + CAST(@WarningCount AS VARCHAR) + N'</td></tr>
        <tr><td>Non-Compliant</td><td style="color:red;font-weight:bold;">' + CAST(@NonCompliantCount AS VARCHAR) + N'</td></tr>
        <tr><td>Unknown</td><td>' + CAST(@UnknownCount AS VARCHAR) + N'</td></tr>
        </table>
        <p>Compliance Rate: ' + CAST(@CompliancePercentage AS VARCHAR) + N'%</p>
        <p style="color:gray;">DBAOps Enterprise™ Framework | John Bosco RHOYLLE INNOVATION EMPIRE™</p>
        </body></html>';
        
        IF @SendEmail = 1
        BEGIN
            EXEC msdb.dbo.sp_send_dbmail
                @profile_name = @MailProfile,
                @recipients = @EmailRecipients,
                @subject = @EmailSubject,
                @body = @EmailBody,
                @body_format = 'HTML';
            PRINT 'DBAOps | JB: Patching compliance email sent';
        END
        
        INSERT INTO Patching.ComplianceReportLog (TotalServers, CompliantServers, NonCompliantServers, WarningServers, UnknownServers, CompliancePercentage, ReportSentTo, ReportStatus)
        VALUES (@TotalServers, @CompliantCount, @NonCompliantCount, @WarningCount, @UnknownCount, @CompliancePercentage, @EmailRecipients, 'Success');
        
    END TRY
    BEGIN CATCH
        PRINT 'Error: ' + ERROR_MESSAGE();
    END CATCH
END;

GO

-- [Patching].[usp_GenerateWeeklyPatchReport]

CREATE PROCEDURE [Patching].[usp_GenerateWeeklyPatchReport]
(
    @SendEmail BIT = 1,
    @Recipients NVARCHAR(500) = NULL
)
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @ToList NVARCHAR(MAX);
    DECLARE @ProfileName SYSNAME;
    DECLARE @Subject NVARCHAR(400);
    DECLARE @BodyHtml NVARCHAR(MAX);
    DECLARE @RunStamp NVARCHAR(30) = CONVERT(NVARCHAR(30), SYSDATETIME(), 120);
    
    -- Get email settings
    IF @Recipients IS NULL
    BEGIN
        SELECT @ToList = ToList, @ProfileName = MailProfileName
        FROM config.EmailDistribution
        WHERE DistributionName = 'DBAOps_PatchingWeekly' AND IsEnabled = 1;
        
        IF @ToList IS NULL
            SELECT @ToList = ToList, @ProfileName = MailProfileName
            FROM config.EmailDistribution
            WHERE DistributionName = 'DBAOps_SqlServicingWeekly' AND IsEnabled = 1;
    END
    ELSE
        SET @ToList = @Recipients;
    
    IF @ProfileName IS NULL
        SELECT TOP 1 @ProfileName = name FROM msdb.dbo.sysmail_profile ORDER BY name;
    
    -- Build temp table from CMS (source of truth)
    CREATE TABLE #PatchStatus
    (
        ServerName NVARCHAR(256),
        Environment NVARCHAR(50),
        SQLVersion NVARCHAR(100),
        CurrentCU NVARCHAR(50),
        TargetCU NVARCHAR(50),
        PatchStatus NVARCHAR(50),
        SupportStatus NVARCHAR(50),
        SupportEnds DATE,
        IsCompliant BIT
    );
    
    INSERT INTO #PatchStatus
    SELECT DISTINCT
        cms.server_name AS ServerName,
        ISNULL(p.Environment, 'UNKNOWN') AS Environment,
        ISNULL(p.SQLVersion, 'Unknown') AS SQLVersion,
        ISNULL(p.CurrentCU, 'N/A') AS CurrentCU,
        ISNULL(p.TargetCU, 'N/A') AS TargetCU,
        ISNULL(p.PatchStatus, 'Unknown') AS PatchStatus,
        ISNULL(p.SupportStatus, 'Unknown') AS SupportStatus,
        p.SupportEnds,
        CASE WHEN p.PatchStatus = 'Up to Date' THEN 1 ELSE 0 END AS IsCompliant
    FROM msdb.dbo.sysmanagement_shared_registered_servers_internal cms
    LEFT JOIN Patching.vw_PatchComplianceDetail p 
        ON UPPER(cms.server_name) = UPPER(p.ServerName);
    
    -- Get summary counts
    DECLARE @Total INT, @Compliant INT, @NonCompliant INT, @Unknown INT;
    
    SELECT 
        @Total = COUNT(*),
        @Compliant = SUM(CASE WHEN IsCompliant = 1 THEN 1 ELSE 0 END),
        @NonCompliant = SUM(CASE WHEN IsCompliant = 0 AND PatchStatus <> 'Unknown' THEN 1 ELSE 0 END),
        @Unknown = SUM(CASE WHEN PatchStatus = 'Unknown' THEN 1 ELSE 0 END)
    FROM #PatchStatus;
    
    -- Only send if there are non-compliant or unknown servers
    IF @NonCompliant = 0 AND @Unknown = 0
    BEGIN
        DROP TABLE #PatchStatus;
        PRINT 'DBAOps | JB: All servers are compliant. Email not sent.';
        
        -- Return summary anyway
        SELECT 
            @Total AS TotalServers,
            @Compliant AS Compliant,
            @NonCompliant AS NonCompliant,
            @Unknown AS Unknown,
            'All Compliant - No email sent' AS Status;
        RETURN;
    END;
    
    -- Build HTML lists
    DECLARE @NonCompliantList NVARCHAR(MAX) = N'';
    DECLARE @UnknownList NVARCHAR(MAX) = N'';
    DECLARE @EnvSummaryList NVARCHAR(MAX) = N'';
    DECLARE @VersionSummaryList NVARCHAR(MAX) = N'';
    
    -- Non-Compliant servers
    SELECT @NonCompliantList = @NonCompliantList + 
        N'<tr><td style="padding:4px 8px;">' + ServerName + 
        N'</td><td style="padding:4px 8px;">' + Environment + 
        N'</td><td style="padding:4px 8px;">' + SQLVersion + 
        N'</td><td style="padding:4px 8px;">' + CurrentCU + 
        N'</td><td style="padding:4px 8px;">' + TargetCU + 
        N'</td><td style="padding:4px 8px;">' + PatchStatus + 
        N'</td></tr>'
    FROM #PatchStatus
    WHERE IsCompliant = 0 AND PatchStatus <> 'Unknown'
    ORDER BY Environment, ServerName;
    
    -- Unknown servers
    SELECT @UnknownList = @UnknownList + 
        N'<tr><td style="padding:4px 8px;">' + ServerName + 
        N'</td><td style="padding:4px 8px;">' + Environment + 
        N'</td><td style="padding:4px 8px;">' + SQLVersion + 
        N'</td><td style="padding:4px 8px;">' + CurrentCU + 
        N'</td></tr>'
    FROM #PatchStatus
    WHERE PatchStatus = 'Unknown'
    ORDER BY ServerName;
    
    -- Environment Summary
    SELECT @EnvSummaryList = @EnvSummaryList + 
        N'<tr><td style="padding:4px 8px;">' + Environment + 
        N'</td><td style="padding:4px 8px;">' + CAST(COUNT(*) AS NVARCHAR(10)) + 
        N'</td><td style="padding:4px 8px;">' + CAST(SUM(CAST(IsCompliant AS INT)) AS NVARCHAR(10)) + 
        N'</td><td style="padding:4px 8px;">' + CAST(SUM(CASE WHEN IsCompliant = 0 THEN 1 ELSE 0 END) AS NVARCHAR(10)) + 
        N'</td><td style="padding:4px 8px;">' + CAST(CAST(SUM(CAST(IsCompliant AS INT)) * 100.0 / NULLIF(COUNT(*), 0) AS DECIMAL(5,1)) AS NVARCHAR(10)) + N'%' +
        N'</td></tr>'
    FROM #PatchStatus
    GROUP BY Environment
    ORDER BY Environment;
    
    -- Version Summary
    SELECT @VersionSummaryList = @VersionSummaryList + 
        N'<tr><td style="padding:4px 8px;">' + SQLVersion + 
        N'</td><td style="padding:4px 8px;">' + CAST(COUNT(*) AS NVARCHAR(10)) + 
        N'</td><td style="padding:4px 8px;">' + CAST(SUM(CAST(IsCompliant AS INT)) AS NVARCHAR(10)) + 
        N'</td><td style="padding:4px 8px;">' + MAX(TargetCU) + 
        N'</td><td style="padding:4px 8px;">' + MAX(SupportStatus) + 
        N'</td></tr>'
    FROM #PatchStatus
    GROUP BY SQLVersion
    ORDER BY SQLVersion DESC;
    
    SET @Subject = N'DBAOps | JB | Weekly Patching Intelligence Report | ' + @RunStamp;
    
    -- Build HTML body
    SET @BodyHtml = N'<html><head><style>
        body { font-family: Segoe UI, Arial, sans-serif; font-size: 12px; }
        h2 { color: #2E86AB; margin-top: 20px; margin-bottom: 10px; border-bottom: 2px solid #2E86AB; padding-bottom: 5px; }
        h3 { color: #A23B72; margin-top: 15px; margin-bottom: 8px; }
        table { border-collapse: collapse; margin: 10px 0; }
        th { background-color: #2E86AB; color: white; padding: 6px 10px; text-align: left; }
        td { padding: 4px 10px; border-bottom: 1px solid #ddd; }
        tr:nth-child(even) { background-color: #f9f9f9; }
        .count-good { color: green; font-weight: bold; }
        .count-warning { color: orange; font-weight: bold; }
        .count-critical { color: red; font-weight: bold; }
    </style></head><body>';
    
    -- Header
    SET @BodyHtml = @BodyHtml + 
        N'<h2>DBAOps | JB | Weekly Patching Intelligence Report</h2>' +
        N'<p><b>Generated:</b> ' + @RunStamp + N'<br/>' +
        N'<b>Author:</b> RHOYLLE INNOVATION EMPIRE™, Sr SQL DBA - DBAOps Engineer<br/>' +
        N'<b>Source:</b> CMS (Central Management Server)</p>';
    
    -- Overall Summary
    SET @BodyHtml = @BodyHtml + 
        N'<h3>Overall Summary</h3>' +
        N'<table>' +
        N'<tr><td><b>Total Servers:</b></td><td>' + CAST(@Total AS NVARCHAR(10)) + N'</td></tr>' +
        N'<tr><td><b>Compliant:</b></td><td class="count-good">' + CAST(@Compliant AS NVARCHAR(10)) + N'</td></tr>' +
        N'<tr><td><b>Non-Compliant:</b></td><td class="count-critical">' + CAST(@NonCompliant AS NVARCHAR(10)) + N'</td></tr>' +
        N'<tr><td><b>Unknown:</b></td><td class="count-warning">' + CAST(@Unknown AS NVARCHAR(10)) + N'</td></tr>' +
        N'</table>';
    
    -- Environment Summary
    SET @BodyHtml = @BodyHtml + 
        N'<h3>Summary by Environment</h3>' +
        N'<table><tr><th>Environment</th><th>Total</th><th>Compliant</th><th>Non-Compliant</th><th>Compliance %</th></tr>' +
        @EnvSummaryList +
        N'</table>';
    
    -- Version Summary
    SET @BodyHtml = @BodyHtml + 
        N'<h3>Summary by SQL Version</h3>' +
        N'<table><tr><th>SQL Version</th><th>Total</th><th>Up to Date</th><th>Target CU</th><th>Support Status</th></tr>' +
        @VersionSummaryList +
        N'</table>';
    
    -- Non-Compliant Details
    IF @NonCompliant > 0
    BEGIN
        SET @BodyHtml = @BodyHtml + 
            N'<h3 style="color:red;">Non-Compliant Servers (' + CAST(@NonCompliant AS NVARCHAR(10)) + N')</h3>' +
            N'<table><tr><th>Server</th><th>Environment</th><th>SQL Version</th><th>Current CU</th><th>Target CU</th><th>Status</th></tr>' +
            @NonCompliantList +
            N'</table>';
    END;
    
    -- Unknown Details
    IF @Unknown > 0
    BEGIN
        SET @BodyHtml = @BodyHtml + 
            N'<h3 style="color:orange;">Unknown Status Servers (' + CAST(@Unknown AS NVARCHAR(10)) + N')</h3>' +
            N'<table><tr><th>Server</th><th>Environment</th><th>SQL Version</th><th>Current CU</th></tr>' +
            @UnknownList +
            N'</table>';
    END;
    
    -- Footer
    SET @BodyHtml = @BodyHtml + 
        N'<hr/><p><i>This report only includes servers requiring attention.</i></p>' +
        N'</body></html>';
    
    DROP TABLE #PatchStatus;
    
    IF @SendEmail = 0 OR @ToList IS NULL
    BEGIN
        SELECT 
            @Subject AS Subject,
            @ToList AS Recipients,
            @Total AS TotalServers,
            @Compliant AS Compliant,
            @NonCompliant AS NonCompliant,
            @Unknown AS Unknown;
        RETURN;
    END;
    
    BEGIN TRY
        EXEC msdb.dbo.sp_send_dbmail
            @profile_name = @ProfileName,
            @recipients   = @ToList,
            @subject      = @Subject,
            @body         = @BodyHtml,
            @body_format  = 'HTML';
        
        PRINT 'DBAOps | JB: Weekly Patching Intelligence Report sent to ' + @ToList;
    END TRY
    BEGIN CATCH
        PRINT 'DBAOps | JB: Email failed - ' + ERROR_MESSAGE();
        THROW;
    END CATCH;
END;

GO

-- [Patching].[usp_GetLatestKBInfo]

-- ============================================================
-- 10. GET MICROSOFT KB INFO
-- ============================================================
CREATE   PROCEDURE Patching.usp_GetLatestKBInfo
    @SQLMajorVersion INT = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    SELECT 
        kb.KBID,
        kb.KBNumber,
        kb.SQLMajorVersion,
        kb.SQLVersionName,
        kb.PatchType,
        kb.CumulativeUpdate,
        kb.BuildNumber,
        kb.ReleaseDate,
        kb.IsSecurityUpdate,
        kb.CVENumbers,
        kb.SecuritySeverity,
        kb.DownloadURL,
        kb.Title,
        kb.IsLatest,
        (SELECT COUNT(*) FROM config.ServerInventory cms 
         WHERE cms.SQLVersion LIKE CAST(kb.SQLMajorVersion AS VARCHAR) + '.%' 
         AND cms.IsActive = 1) AS AffectedServerCount
    FROM Patching.MicrosoftKBReference kb
    WHERE (@SQLMajorVersion IS NULL OR kb.SQLMajorVersion = @SQLMajorVersion)
    AND kb.IsLatest = 1
    ORDER BY kb.SQLMajorVersion DESC;
END

GO

-- [Patching].[usp_GetNextPatchToExecute]

-- ============================================================
-- 3. PROCEDURE: Get next patch to execute
-- Called by SQL Agent job
-- ============================================================
CREATE   PROCEDURE Patching.usp_GetNextPatchToExecute
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @QueueID INT;
    
    -- Get highest priority queued item that's ready to run
    SELECT TOP 1 @QueueID = QueueID
    FROM Patching.PatchExecutionQueue
    WHERE QueueStatus = 'Queued'
    AND (ScheduledExecutionTime IS NULL OR ScheduledExecutionTime <= GETDATE())
    ORDER BY Priority ASC, QueuedDate ASC;
    
    IF @QueueID IS NOT NULL
    BEGIN
        -- Mark as running
        UPDATE Patching.PatchExecutionQueue
        SET QueueStatus = 'Running', StartedDate = GETDATE()
        WHERE QueueID = @QueueID;
        
        -- Return the patch details
        SELECT 
            QueueID, ScheduleID, ServerName, InstanceName,
            PatchFilePath, KBNumber, PatchType
        FROM Patching.PatchExecutionQueue
        WHERE QueueID = @QueueID;
    END
    ELSE
    BEGIN
        -- No patches ready
        SELECT NULL AS QueueID;
    END
END

GO

-- [Patching].[usp_GetPatchCalendar]

-- ============================================================
-- 4. GET CALENDAR DATA
-- ============================================================
CREATE   PROCEDURE Patching.usp_GetPatchCalendar
    @Year INT = NULL,
    @Month INT = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    SET @Year = ISNULL(@Year, YEAR(GETDATE()));
    
    SELECT 
        c.CalendarDate,
        c.CalendarDay,
        c.DayOfWeek,
        c.WeekOfMonth,
        c.IsWeekend,
        c.IsHoliday,
        c.HolidayName,
        c.IsPatchTuesday,
        (SELECT COUNT(*) FROM Patching.ScheduledPatches s 
         WHERE s.ScheduledDate = c.CalendarDate AND s.Status NOT IN ('Cancelled', 'Completed')) AS ScheduledPatchCount,
        (SELECT COUNT(*) FROM Patching.ScheduledPatches s 
         WHERE s.ScheduledDate = c.CalendarDate AND s.Status = 'Pending') AS PendingCount,
        (SELECT COUNT(*) FROM Patching.ScheduledPatches s 
         WHERE s.ScheduledDate = c.CalendarDate AND s.Status = 'Approved') AS ApprovedCount
    FROM Patching.PatchCalendar c
    WHERE c.CalendarYear = @Year
    AND (@Month IS NULL OR c.CalendarMonth = @Month)
    ORDER BY c.CalendarDate;
END

GO

-- [Patching].[usp_GetPatchGroups]

-- ============================================================
-- 8. GET PATCH GROUPS
-- ============================================================
CREATE   PROCEDURE Patching.usp_GetPatchGroups
AS
BEGIN
    SET NOCOUNT ON;
    
    SELECT 
        g.GroupID,
        g.GroupName,
        g.GroupDescription,
        g.Environment,
        g.ComplianceTier,
        g.PatchPriority,
        g.RequiresApproval,
        g.RequiresChangeTicket,
        g.MaxConcurrentPatches,
        g.PatchSequenceOrder,
        g.IsActive,
        (SELECT COUNT(*) FROM Patching.ServerGroupMembers m WHERE m.GroupID = g.GroupID AND m.IsActive = 1) AS MemberCount
    FROM Patching.ServerPatchGroups g
    WHERE g.IsActive = 1
    ORDER BY g.PatchSequenceOrder;
END

GO

-- [Patching].[usp_GetPatchingStatus]

CREATE   PROCEDURE [Patching].[usp_GetPatchingStatus]
    @Environment NVARCHAR(50) = NULL,
    @ServerName NVARCHAR(128) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    SELECT 
        cms.ServerName,
        COALESCE(cms.Environment, cms.CMSEnvironment, 'Unknown') AS Environment,
        cms.SQLVersion,
        cms.SqlBuild AS CurrentBuild,
        cms.SqlCumulativeUpdate AS CurrentCU,
        br.LatestCU AS TargetCU,
        br.LatestBuildNumber AS TargetBuild,
        CASE 
            WHEN cms.SqlCumulativeUpdate = br.LatestCU THEN 'Current'
            WHEN cms.SqlCumulativeUpdate IS NULL THEN 'Unknown'
            ELSE 'Needs Update'
        END AS PatchStatus,
        br.EndOfSupport,
        br.IsSupported,
        cms.LastPatchDate,
        cms.ComplianceTier
    FROM config.ServerInventory cms
    LEFT JOIN Patching.SQLServerBuildReference br 
        ON cms.SQLVersion LIKE CAST(br.SQLMajorVersion AS VARCHAR) + '.%'
    WHERE cms.IsActive = 1
    AND (cms.ServerType = 'SQL Server' OR cms.ServerType IS NULL)
    AND (@Environment IS NULL OR cms.Environment = @Environment OR cms.CMSEnvironment = @Environment)
    AND (@ServerName IS NULL OR cms.ServerName = @ServerName)
    ORDER BY 
        CASE WHEN br.IsSupported = 0 THEN 1 ELSE 2 END,
        cms.Environment,
        cms.ServerName;
END

GO

-- [Patching].[usp_GetPendingPatches]

CREATE PROCEDURE Patching.usp_GetPendingPatches
AS
BEGIN
    SET NOCOUNT ON;
    SELECT * FROM Patching.vw_PendingPatchRequests ORDER BY ScheduledStart;
END

GO

-- [Patching].[usp_GetScheduledPatchesForDate]

-- ============================================================
-- 5. GET SCHEDULED PATCHES FOR DATE
-- ============================================================
CREATE   PROCEDURE Patching.usp_GetScheduledPatchesForDate
    @Date DATE
AS
BEGIN
    SET NOCOUNT ON;
    
    SELECT 
        s.ScheduleID,
        s.ScheduleName,
        s.ScheduleType,
        s.ServerName,
        g.GroupName,
        s.Environment,
        s.ComplianceTier,
        s.PatchType,
        s.TargetCU,
        s.KBNumber,
        s.MicrosoftDownloadURL,
        s.ScheduledStartTime,
        s.ScheduledEndTime,
        s.Status,
        s.ApprovalStatus,
        s.ApprovedBy,
        s.ChangeTicketNumber,
        s.CreatedBy,
        s.CreatedDate
    FROM Patching.ScheduledPatches s
    LEFT JOIN Patching.ServerPatchGroups g ON s.GroupID = g.GroupID
    WHERE s.ScheduledDate = @Date
    ORDER BY s.ScheduledStartTime;
END

GO

-- [Patching].[usp_PostCollectionStatusFix]

CREATE   PROCEDURE [Patching].[usp_PostCollectionStatusFix]
AS
BEGIN
    SET NOCOUNT ON;
    
    -- Just sync new servers from CMS to ServerPatchStatus
    INSERT INTO Patching.ServerPatchStatus (ServerName)
    SELECT cms.ServerName
    FROM config.ServerInventory cms
    WHERE cms.IsActive = 1
    AND (cms.ServerType = 'SQL Server' OR cms.ServerType IS NULL)
    AND NOT EXISTS (
        SELECT 1 FROM Patching.ServerPatchStatus sps 
        WHERE sps.ServerName = cms.ServerName
    );
    
    PRINT 'Post-collection sync completed - ' + CAST(@@ROWCOUNT AS VARCHAR) + ' servers added from CMS';
END

GO

-- [Patching].[usp_QueuePatchExecution]

-- ============================================================
-- 2. PROCEDURE: Queue a patch for execution
-- Called by dashboard when admin clicks "Execute Now"
-- ============================================================
CREATE   PROCEDURE Patching.usp_QueuePatchExecution
    @ScheduleID INT = NULL,
    @ServerName VARCHAR(128),
    @InstanceName VARCHAR(128) = 'MSSQLSERVER',
    @PatchFilePath VARCHAR(500),
    @KBNumber VARCHAR(20) = NULL,
    @PatchType VARCHAR(50) = 'CU',
    @ScheduledExecutionTime DATETIME = NULL,
    @Priority INT = 5,
    @QueueID INT OUTPUT
AS
BEGIN
    SET NOCOUNT ON;
    
    -- Validate server exists
    IF NOT EXISTS (SELECT 1 FROM config.ServerInventory WHERE ServerName = @ServerName AND IsActive = 1)
    BEGIN
        RAISERROR('Server not found in inventory: %s', 16, 1, @ServerName);
        RETURN -1;
    END
    
    -- Check for existing queued patch for same server
    IF EXISTS (SELECT 1 FROM Patching.PatchExecutionQueue 
               WHERE ServerName = @ServerName AND QueueStatus IN ('Queued', 'Running'))
    BEGIN
        RAISERROR('Server %s already has a pending patch in queue', 16, 1, @ServerName);
        RETURN -2;
    END
    
    -- Insert into queue
    INSERT INTO Patching.PatchExecutionQueue (
        ScheduleID, ServerName, InstanceName, PatchFilePath, 
        KBNumber, PatchType, ScheduledExecutionTime, Priority
    )
    VALUES (
        @ScheduleID, @ServerName, @InstanceName, @PatchFilePath,
        @KBNumber, @PatchType, @ScheduledExecutionTime, @Priority
    );
    
    SET @QueueID = SCOPE_IDENTITY();
    
    -- Update schedule status if linked
    IF @ScheduleID IS NOT NULL
    BEGIN
        UPDATE Patching.ScheduledPatches 
        SET Status = 'Queued', ModifiedDate = GETDATE(), ModifiedBy = SUSER_SNAME()
        WHERE ScheduleID = @ScheduleID;
    END
    
    PRINT 'Patch queued: QueueID=' + CAST(@QueueID AS VARCHAR) + ', Server=' + @ServerName;
    RETURN 0;
END

GO

-- [Patching].[usp_QuickDeploy]

-- â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
-- SECTION 19: PROCEDURE - QUICK DEPLOY
-- â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
CREATE   PROCEDURE Patching.usp_QuickDeploy
    @ServerList NVARCHAR(MAX),
    @SQLVersion NVARCHAR(20) = '2022',
    @RunNow BIT = 0,
    @SendNotification BIT = 1
AS
BEGIN
    SET NOCOUNT ON;
    
    PRINT 'â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•';
    PRINT '  DBAOps Quick CU Deployment';
    PRINT 'â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•';
    PRINT '';
    
    PRINT 'Step 1: Adding servers to queue...';
    EXEC Patching.usp_AddToQueue @ServerList = @ServerList, @SQLVersion = @SQLVersion, @AutoApprove = 1;
    
    PRINT '';
    PRINT 'Step 2: Current queue:';
    SELECT ServerName, TargetCU, KBNumber, Status, Environment
    FROM Patching.vw_DeploymentQueue
    WHERE Status = 'Approved'
    ORDER BY Priority;
    
    IF @RunNow = 1
    BEGIN
        PRINT '';
        PRINT 'Step 3: Starting deployment job...';
        BEGIN TRY
            EXEC msdb.dbo.sp_start_job @job_name = N'DBAOps - CU Deployment Engine';
            PRINT '  âœ“ Job started. Check job history for progress.';
        END TRY
        BEGIN CATCH
            PRINT '  Warning: Could not start job - ' + ERROR_MESSAGE();
        END CATCH
    END
    ELSE
    BEGIN
        PRINT '';
        PRINT 'Step 3: Deployment scheduled for next maintenance window.';
    END
END;

GO

-- [Patching].[usp_RecordDeploymentComplete]

-- â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
-- SECTION 14: PROCEDURE - RECORD DEPLOYMENT COMPLETE
-- â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
CREATE   PROCEDURE Patching.usp_RecordDeploymentComplete
    @DeploymentID INT,
    @PostBuild NVARCHAR(50),
    @ExitCode INT,
    @RebootRequired BIT = 0,
    @ErrorMessage NVARCHAR(MAX) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @Status NVARCHAR(30);
    DECLARE @TargetBuild NVARCHAR(50);
    DECLARE @QueueID INT;
    DECLARE @BatchID UNIQUEIDENTIFIER;
    
    SELECT @TargetBuild = TargetBuild, @QueueID = QueueID, @BatchID = BatchID
    FROM Patching.DeploymentHistory WHERE DeploymentID = @DeploymentID;
    
    SET @Status = CASE 
        WHEN @ErrorMessage IS NOT NULL THEN 'Failed'
        WHEN @ExitCode = 0 AND @PostBuild >= @TargetBuild THEN 'Success'
        WHEN @ExitCode = 3010 THEN 'Success-RebootRequired'
        WHEN @PostBuild >= @TargetBuild THEN 'Success'
        ELSE 'Completed-Verify'
    END;
    
    UPDATE Patching.DeploymentHistory
    SET PostDeployBuild = @PostBuild,
        PostDeployCU = Patching.fn_GetCUFromBuild(@PostBuild),
        DeploymentStatus = @Status,
        DeploymentEndTime = GETDATE(),
        ExitCode = @ExitCode,
        RebootRequired = @RebootRequired,
        ErrorMessage = @ErrorMessage
    WHERE DeploymentID = @DeploymentID;
    
    UPDATE Patching.DeploymentQueue 
    SET Status = CASE WHEN @Status LIKE 'Success%' OR @Status = 'Completed-Verify' THEN 'Completed' ELSE 'Failed' END
    WHERE QueueID = @QueueID;
    
    UPDATE Patching.DeploymentBatches
    SET SuccessCount = SuccessCount + CASE WHEN @Status LIKE 'Success%' THEN 1 ELSE 0 END,
        FailedCount = FailedCount + CASE WHEN @Status = 'Failed' THEN 1 ELSE 0 END,
        RebootRequiredCount = RebootRequiredCount + CASE WHEN @RebootRequired = 1 THEN 1 ELSE 0 END
    WHERE BatchID = @BatchID;
END;

GO

-- [Patching].[usp_RecordDeploymentStart]

-- â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
-- SECTION 13: PROCEDURE - RECORD DEPLOYMENT START
-- â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
CREATE   PROCEDURE Patching.usp_RecordDeploymentStart
    @QueueID INT,
    @BatchID UNIQUEIDENTIFIER,
    @PreBuild NVARCHAR(50),
    @OSVersion NVARCHAR(256) = NULL,
    @OSBuild NVARCHAR(50) = NULL,
    @DiskFreeGB DECIMAL(10,2) = NULL,
    @DeploymentID INT OUTPUT
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @ServerName NVARCHAR(128);
    DECLARE @PackageID INT;
    DECLARE @TargetBuild NVARCHAR(50);
    DECLARE @TargetCU NVARCHAR(20);
    DECLARE @TargetKB NVARCHAR(20);
    
    SELECT @ServerName = q.ServerName, @PackageID = q.PackageID,
           @TargetBuild = p.BuildNumber, @TargetCU = p.CUName, @TargetKB = p.KBNumber
    FROM Patching.DeploymentQueue q
    JOIN Patching.CUPackages p ON q.PackageID = p.PackageID
    WHERE q.QueueID = @QueueID;
    
    INSERT INTO Patching.DeploymentHistory (
        QueueID, ServerName, PackageID, BatchID,
        PreDeployBuild, PreDeployCU, PreDeployDate,
        TargetCU, TargetKB, TargetBuild,
        OSVersion, OSBuild, DiskFreeGB,
        DeploymentStatus, DeploymentStartTime
    )
    VALUES (
        @QueueID, @ServerName, @PackageID, @BatchID,
        @PreBuild, Patching.fn_GetCUFromBuild(@PreBuild), GETDATE(),
        @TargetCU, @TargetKB, @TargetBuild,
        @OSVersion, @OSBuild, @DiskFreeGB,
        'InProgress', GETDATE()
    );
    
    SET @DeploymentID = SCOPE_IDENTITY();
    UPDATE Patching.DeploymentQueue SET Status = 'InProgress' WHERE QueueID = @QueueID;
END;

GO

-- [Patching].[usp_SafeCheckNewReleases]

CREATE PROCEDURE Patching.usp_SafeCheckNewReleases
AS
BEGIN
    SET NOCOUNT ON;
    
    PRINT '--------------------------------------------------------------------------';
    PRINT 'DBAOps Release Detection System (SAFE VERSION)';
    PRINT 'Time: ' + CONVERT(VARCHAR, GETDATE(), 120);
    PRINT '--------------------------------------------------------------------------';
    PRINT '';
    
    -- Get servers with builds newer than tracked releases
    -- Using SAFE CU number extraction
    
    PRINT 'Checking server builds against tracked releases...';
    
    ;WITH ServerBuilds AS (
        SELECT 
            si.ServerName,
            si.SQLVersion,
            vi.VersionName,
            vi.MajorVersion,
            br.CumulativeUpdate AS TrackedCU,
            dbo.fn_ExtractCUNumber(br.CumulativeUpdate) AS TrackedCUNumber,
            br.BuildNumber AS TrackedBuild,
            br.IsLatest
        FROM config.ServerInventory si
        CROSS APPLY dbo.fn_GetSQLVersionInfo(si.SQLVersion) vi
        LEFT JOIN Patching.SQLBuildReference br 
            ON vi.MajorVersion = br.SQLMajorVersion 
            AND br.IsLatest = 1
        WHERE si.IsActive = 1
          AND si.SQLVersion IS NOT NULL
    )
    SELECT 
        VersionName,
        COUNT(*) AS ServerCount,
        TrackedCU AS LatestTrackedCU,
        TrackedBuild AS LatestTrackedBuild
    FROM ServerBuilds
    WHERE SQLVersion > TrackedBuild  -- Server has newer build
    GROUP BY VersionName, TrackedCU, TrackedBuild
    ORDER BY VersionName;
    
    -- Check for higher CU numbers safely
    PRINT '';
    PRINT 'Checking for servers with higher CU than tracked...';
    
    SELECT 
        si.ServerName,
        si.SQLVersion,
        vi.VersionName,
        br.CumulativeUpdate AS TrackedCU,
        br.BuildNumber AS TrackedBuild
    FROM config.ServerInventory si
    CROSS APPLY dbo.fn_GetSQLVersionInfo(si.SQLVersion) vi
    LEFT JOIN Patching.SQLBuildReference br 
        ON vi.MajorVersion = br.SQLMajorVersion 
        AND br.IsLatest = 1
    WHERE si.IsActive = 1
      AND si.SQLVersion IS NOT NULL
      AND si.SQLVersion > ISNULL(br.BuildNumber, '0')
    ORDER BY vi.VersionName, si.ServerName;
    
    PRINT '';
    PRINT 'Release check complete.';
END

GO

-- [Patching].[usp_SchedulePatch]
CREATE OR ALTER PROCEDURE Patching.usp_SchedulePatch
    @ServerName NVARCHAR(128),
    @ScheduledDate DATETIME = NULL,
    @MaintenanceWindowID INT = NULL,
    @PatchType VARCHAR(20) = 'CU',
    @Priority VARCHAR(20) = 'Normal',
    @ChangeTicket VARCHAR(50) = '',
    @Notes VARCHAR(500) = '',
    @CreatedBy VARCHAR(100) = 'Dashboard'
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @TargetCU VARCHAR(20);
    DECLARE @KBArticle VARCHAR(20);
    DECLARE @PatchFilePath VARCHAR(500);
    
    SELECT @TargetCU = CASE 
        WHEN SQLVersion LIKE '16.%' THEN 'CU22'
        WHEN SQLVersion LIKE '15.%' THEN 'CU32+GDR'
        WHEN SQLVersion LIKE '14.%' THEN 'CU31+GDR'
        WHEN SQLVersion LIKE '13.%' THEN 'SP3+GDR'
        ELSE 'Latest'
    END,
    @KBArticle = CASE 
        WHEN SQLVersion LIKE '16.%' THEN 'KB5068450'
        WHEN SQLVersion LIKE '15.%' THEN 'KB5068404'
        WHEN SQLVersion LIKE '14.%' THEN 'KB5068403'
        WHEN SQLVersion LIKE '13.%' THEN 'KB5068401'
        ELSE 'Unknown'
    END,
    @PatchFilePath = CASE 
        WHEN SQLVersion LIKE '16.%' THEN 'E:\Patches\SQL2022\sqlserver2022-kb5068450-x64_70c032e1bd645698a0048c2cccb2e7f8faa1b8c7.exe'
        WHEN SQLVersion LIKE '15.%' THEN 'E:\Patches\SQL2019\sqlserver2019-kb5068404-x64_745b3298ef3ef6ac4e67b4b7ccc80278632fa9e5.exe'
        WHEN SQLVersion LIKE '13.%' THEN 'E:\Patches\SQL2016\sqlserver2016-kb5068401-x64_17127e890f7d66f017bd760768ee8fdfd24e81d3.exe'
        ELSE 'TBD'
    END
    FROM config.ServerInventory WHERE ServerName = @ServerName;
    
    INSERT INTO Patching.PatchRequests (
        RequestType, ServerName, TargetCU, KBArticle,
        MaintenanceWindowID, ScheduledStart, 
        Notes, RequestedBy, RequestedDate, Status
    )
    VALUES (
        'Single', @ServerName, @TargetCU, @KBArticle,
        @MaintenanceWindowID, ISNULL(@ScheduledDate, GETDATE()),
        @Notes, @CreatedBy, GETDATE(), 'Pending'
    );
    
    DECLARE @RequestID INT = SCOPE_IDENTITY();
    
    IF @ScheduledDate IS NULL OR @ScheduledDate <= DATEADD(MINUTE, 5, GETDATE())
    BEGIN
        INSERT INTO Patching.PatchExecutionQueue (
            ServerName, PatchFilePath, KBNumber, PatchType, QueueStatus, 
            Priority, QueuedDate, QueuedBy, ScheduledExecutionTime
        )
        SELECT @ServerName, @PatchFilePath, @KBArticle, @PatchType, 'Queued', 
               1, GETDATE(), @CreatedBy, GETDATE()
        WHERE NOT EXISTS (
            SELECT 1 FROM Patching.PatchExecutionQueue 
            WHERE ServerName = @ServerName AND QueueStatus IN ('Queued', 'Running')
        );
    END
    
    SELECT @RequestID AS RequestID, @TargetCU AS TargetCU, @KBArticle AS KBArticle;
END
GO

-- [Patching].[usp_SendDeploymentReport]

CREATE   PROCEDURE [Patching].[usp_SendDeploymentReport]
    @BatchID UNIQUEIDENTIFIER = NULL,
    @DaysBack INT = 7
AS
BEGIN
    SET NOCOUNT ON;
    
    SELECT 
        h.DeploymentID,
        h.ServerName,
        COALESCE(cms.Environment, cms.CMSEnvironment, 'Unknown') AS Environment,
        cms.ComplianceTier,
        h.PreDeployCU,
        h.PostDeployCU,
        h.DeploymentStatus,
        h.DeploymentStartTime,
        h.DeploymentEndTime,
        h.DurationMinutes,
        h.RebootRequired,
        h.DeployedBy,
        h.ErrorMessage
    FROM Patching.DeploymentHistory h
    LEFT JOIN config.ServerInventory cms ON h.ServerName = cms.ServerName
    WHERE (@BatchID IS NULL OR h.BatchID = @BatchID)
    AND h.DeploymentStartTime >= DATEADD(DAY, -@DaysBack, GETDATE())
    ORDER BY h.DeploymentStartTime DESC;
END

GO

-- [Patching].[usp_SendReleaseNotification]

-- â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
-- Procedure: usp_SendReleaseNotification
-- Purpose: Send notification email when new release detected
-- â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
CREATE PROCEDURE Patching.usp_SendReleaseNotification
    @SQLMajorVersion INT,
    @SQLVersionName NVARCHAR(50),
    @NewRelease NVARCHAR(20),
    @NewKB NVARCHAR(20),
    @NewBuild NVARCHAR(20),
    @NewReleaseDate DATE = NULL,
    @CurrentRelease NVARCHAR(20),
    @CurrentKB NVARCHAR(20),
    @DetectionID INT = NULL,
    @TestMode BIT = 0,
    @TestEmailAddress NVARCHAR(256) = NULL
AS
BEGIN
    /*
    â•”â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•—
    â•‘ DBAOps Monitoring and Compliance Framework                                â•‘
    â•‘ Procedure: usp_SendReleaseNotification                                    â•‘
    â•‘ Author: John Bosco RHOYLLE INNOVATION EMPIRE™, Sr SQL DBA/DBAOps Engineer                   â•‘
    â•šâ•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
    */
    SET NOCOUNT ON;
    
    DECLARE @EmailRecipients NVARCHAR(MAX);
    DECLARE @EmailSubject NVARCHAR(256);
    DECLARE @EmailBody NVARCHAR(MAX);
    DECLARE @MailProfile NVARCHAR(128);
    DECLARE @FrameworkName NVARCHAR(100);
    DECLARE @FrameworkAuthor NVARCHAR(200);
    DECLARE @DownloadURL NVARCHAR(500);
    
    -- Get configuration
    SELECT @EmailRecipients = ConfigValue FROM Patching.ComplianceConfiguration WHERE ConfigKey = 'EmailRecipients';
    SELECT @MailProfile = ISNULL(ConfigValue, 'DBAOpsProfile') FROM Patching.ComplianceConfiguration WHERE ConfigKey = 'MailProfile';
    SELECT @FrameworkName = ConfigValue FROM Patching.ComplianceConfiguration WHERE ConfigKey = 'FrameworkName';
    SELECT @FrameworkAuthor = ConfigValue FROM Patching.ComplianceConfiguration WHERE ConfigKey = 'FrameworkAuthor';
    
    IF @TestMode = 1 AND @TestEmailAddress IS NOT NULL
        SET @EmailRecipients = @TestEmailAddress;
    
    -- Set download URL
    SET @DownloadURL = CASE @SQLMajorVersion
        WHEN 16 THEN 'https://www.microsoft.com/download/details.aspx?id=105013'
        WHEN 15 THEN 'https://www.microsoft.com/download/details.aspx?id=100809'
        ELSE 'https://www.microsoft.com/en-us/sql-server/sql-server-downloads'
    END;
    
    -- Build subject
    SET @EmailSubject = 'ðŸ”” NEW RELEASE DETECTED: ' + @SQLVersionName + ' ' + @NewRelease + ' (' + @NewKB + ') - Approval Required';
    
    -- Build HTML email
    SET @EmailBody = N'<!DOCTYPE html><html><head><style>
body{font-family:Segoe UI,Arial,sans-serif;margin:0;padding:20px;background:#f5f7fa}
.container{max-width:800px;margin:0 auto;background:#fff;border-radius:12px;box-shadow:0 4px 20px rgba(0,0,0,.1);overflow:hidden}
.header{background:linear-gradient(135deg,#059669 0%,#10b981 100%);color:#fff;padding:30px;text-align:center}
.header h1{margin:0;font-size:24px}
.header .subtitle{opacity:.9;font-size:14px;margin-top:10px}
.content{padding:30px}
.alert-box{background:linear-gradient(135deg,#fef3c7,#fde68a);border-left:5px solid #f59e0b;padding:20px;border-radius:0 8px 8px 0;margin:20px 0}
.alert-box h3{margin:0 0 10px 0;color:#92400e}
.release-card{background:#f8fafc;border:2px solid #e2e8f0;border-radius:10px;padding:25px;margin:20px 0}
.release-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}
.release-title{font-size:1.4em;font-weight:700;color:#1e3a5f}
.release-kb{background:#3b82f6;color:#fff;padding:8px 16px;border-radius:20px;font-weight:600}
.details-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:15px}
.detail-item{background:#fff;padding:15px;border-radius:8px;border:1px solid #e2e8f0}
.detail-label{font-size:.8em;color:#666;margin-bottom:5px}
.detail-value{font-size:1.1em;font-weight:600;color:#1e3a5f}
.detail-value.new{color:#059669}
.detail-value.old{color:#9ca3af}
.arrow{font-size:1.5em;color:#059669;text-align:center;padding:10px}
.approval-section{background:#eff6ff;border:2px solid #3b82f6;border-radius:10px;padding:25px;margin:25px 0}
.approval-section h3{margin:0 0 15px 0;color:#1e40af}
.code-block{background:#1e293b;color:#e2e8f0;padding:20px;border-radius:8px;font-family:Consolas,Monaco,monospace;font-size:.9em;overflow-x:auto;white-space:pre-wrap;margin:15px 0}
.btn{display:inline-block;padding:12px 25px;border-radius:6px;text-decoration:none;font-weight:600;margin:5px}
.btn-primary{background:linear-gradient(135deg,#3b82f6,#2563eb);color:#fff}
.btn-success{background:linear-gradient(135deg,#059669,#10b981);color:#fff}
.warning-box{background:#fef2f2;border-left:4px solid #dc2626;padding:15px;border-radius:0 8px 8px 0;margin:20px 0}
.info-box{background:#eff6ff;border-left:4px solid #3b82f6;padding:15px;border-radius:0 8px 8px 0;margin:20px 0}
.footer{background:#f8fafc;padding:20px 30px;border-top:1px solid #e2e8f0;font-size:.85em;color:#64748b}
table{width:100%;border-collapse:collapse;margin:15px 0}
th,td{padding:10px;text-align:left;border-bottom:1px solid #e2e8f0}
th{background:#f1f5f9;font-weight:600}
</style></head><body>
<div class="container">

<div class="header">
<h1>ðŸ”” New SQL Server Release Detected</h1>
<div class="subtitle">DBAOps Monitoring and Compliance Framework - Release Detection System</div>
</div>

<div class="content">

<div class="alert-box">
<h3>âš ï¸ Action Required: DBA Approval Needed</h3>
<p>A new cumulative update has been detected for ' + @SQLVersionName + '. Please review the details below and approve the update to include it in the patching schedule.</p>
</div>

<div class="release-card">
<div class="release-header">
<span class="release-title">' + @SQLVersionName + ' ' + @NewRelease + '</span>
<span class="release-kb">' + @NewKB + '</span>
</div>

<div class="details-grid">
<div class="detail-item">
<div class="detail-label">NEW BUILD NUMBER</div>
<div class="detail-value new">' + @NewBuild + '</div>
</div>
<div class="detail-item">
<div class="detail-label">RELEASE DATE</div>
<div class="detail-value new">' + ISNULL(FORMAT(@NewReleaseDate, 'MMMM dd, yyyy'), 'Check Microsoft') + '</div>
</div>
<div class="detail-item">
<div class="detail-label">CURRENT VERSION IN SYSTEM</div>
<div class="detail-value old">' + ISNULL(@CurrentRelease, 'None') + ' (' + ISNULL(@CurrentKB, 'N/A') + ')</div>
</div>
<div class="detail-item">
<div class="detail-label">DOWNLOAD</div>
<div class="detail-value"><a href="' + @DownloadURL + '">Microsoft Download Center</a></div>
</div>
</div>
</div>

<div class="info-box">
<strong>ðŸ“‹ What This Means:</strong>
<ul style="margin:10px 0 0 0;padding-left:20px">
<li>Microsoft has released a new cumulative update</li>
<li>Your current system is tracking <strong>' + ISNULL(@CurrentRelease, 'an older version') + '</strong> as the latest</li>
<li>After approval, the Monday report will show servers needing <strong>' + @NewRelease + '</strong></li>
<li>Patching will follow your schedule: Non-Prod Thursday â†’ Production Thursday (7 days later)</li>
</ul>
</div>

<div class="approval-section">
<h3>âœ… To Approve This Release</h3>
<p>Copy and execute the following script in SSMS on <strong>YOURSERVER01</strong>:</p>

<div class="code-block">-- â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
-- DBAOps Release Approval Script
-- Generated: ' + CONVERT(VARCHAR, GETDATE(), 120) + '
-- â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•

USE DBAOpsRepository;
GO

-- Approve and apply the new release
EXEC Patching.usp_ApproveRelease
    @SQLMajorVersion = ' + CAST(@SQLMajorVersion AS VARCHAR) + ',
    @ReleaseName = ''' + @NewRelease + ''',
    @KBNumber = ''' + @NewKB + ''',
    @BuildNumber = ''' + @NewBuild + ''',
    @ReleaseDate = ''' + ISNULL(CONVERT(VARCHAR, @NewReleaseDate, 23), CONVERT(VARCHAR, GETDATE(), 23)) + ''',
    @DownloadURL = ''' + @DownloadURL + ''',
    @SecurityFixes = 0,  -- Update with actual count from release notes
    @BugFixes = 0,       -- Update with actual count from release notes
    @IsCritical = 0,     -- Set to 1 if security-critical
    @ReleaseNotes = NULL,
    @ApprovedBy = ''' + ISNULL(@EmailRecipients, 'DBA Team') + ''';
GO</div>

<p style="margin-top:15px">
<a href="' + @DownloadURL + '" class="btn btn-primary">â¬‡ï¸ Download ' + @NewKB + '</a>
<a href="https://learn.microsoft.com/en-us/troubleshoot/sql/releases/sqlserver-' + 
    CASE @SQLMajorVersion WHEN 16 THEN '2022' WHEN 15 THEN '2019' ELSE '2022' END + 
    '/build-versions" class="btn btn-success">ðŸ“‹ View Release Notes</a>
</p>
</div>

<div class="warning-box">
<strong>âš ï¸ Compliance Note (CJIS/NIST):</strong><br/>
This human-in-the-loop approval process ensures compliance with CJIS Security Policy 5.10.4.1 and NIST SP 800-53 SI-2 by requiring DBA verification before patches are added to the deployment schedule.
</div>

<h3>ðŸ“… Upcoming Patch Schedule</h3>
<table>
<tr><th>Phase</th><th>Date</th><th>Servers</th></tr>
<tr><td>Non-Prod Patching</td><td>Thursday of Patch Tuesday week</td><td>Test/Dev servers</td></tr>
<tr><td>Production Patching</td><td>Following Thursday (7 days later)</td><td>Production servers</td></tr>
</table>

</div>

<div class="footer">
<strong>ðŸ“§ Notification Generated:</strong> ' + FORMAT(GETDATE(), 'yyyy-MM-dd HH:mm:ss') + '<br/>
<strong>ðŸ¢ Framework:</strong> ' + ISNULL(@FrameworkName, 'DBAOps Monitoring and Compliance Framework') + '<br/>
<strong>ðŸ‘¤ Author:</strong> ' + ISNULL(@FrameworkAuthor, 'John Bosco RHOYLLE INNOVATION EMPIRE™, Sr SQL DBA/DBAOps Engineer') + '<br/>
<strong>ðŸ“¬ Recipients:</strong> ' + @EmailRecipients + '<br/><br/>
<em>This is an automated notification from the DBAOps Release Detection System.</em>
</div>

</div></body></html>';

    -- Send email
    BEGIN TRY
        EXEC msdb.dbo.sp_send_dbmail
            @profile_name = @MailProfile,
            @recipients = @EmailRecipients,
            @subject = @EmailSubject,
            @body = @EmailBody,
            @body_format = 'HTML',
            @importance = 'High';
        
        -- Update detection log
        IF @DetectionID IS NOT NULL
        BEGIN
            UPDATE Patching.ReleaseDetectionLog
            SET NotificationSent = 1,
                NotificationDate = GETDATE()
            WHERE DetectionID = @DetectionID;
        END
        
        PRINT '  âœ“ Notification sent to: ' + @EmailRecipients;
    END TRY
    BEGIN CATCH
        PRINT '  âœ— Failed to send notification: ' + ERROR_MESSAGE();
    END CATCH
END

GO

-- [Patching].[usp_StartBatch]

-- â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
-- SECTION 12: PROCEDURE - START BATCH
-- â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
CREATE   PROCEDURE Patching.usp_StartBatch
    @BatchID UNIQUEIDENTIFIER OUTPUT,
    @BatchName NVARCHAR(200) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    SET @BatchID = NEWID();
    
    IF @BatchName IS NULL
        SET @BatchName = 'CU Deployment - ' + CONVERT(VARCHAR, GETDATE(), 120);
    
    INSERT INTO Patching.DeploymentBatches (BatchID, BatchName, TotalServers)
    SELECT @BatchID, @BatchName, COUNT(*)
    FROM Patching.DeploymentQueue WHERE Status = 'Approved';
    
    PRINT '  âœ“ Started batch: ' + CAST(@BatchID AS VARCHAR(50));
END;

GO

-- [Patching].[usp_SyncServersFromA2]

CREATE   PROCEDURE Patching.usp_SyncServersFromA2
AS
BEGIN
    SET NOCOUNT ON;
    
    -- DEPRECATED: This proc now calls the CMS version
    -- Kept for backward compatibility with existing jobs
    PRINT 'NOTE: usp_SyncServersFromA2 is deprecated. Now syncing from CMS (config.ServerInventory)';
    
    -- Call the CMS version
    EXEC Patching.usp_SyncServersFromCMS;
END

GO

-- [Patching].[usp_SyncServersFromCMS]

CREATE   PROCEDURE [Patching].[usp_SyncServersFromCMS]
AS
BEGIN
    SET NOCOUNT ON;
    
    -- Insert new servers from CMS that don't exist in ServerPatchStatus
    INSERT INTO Patching.ServerPatchStatus (ServerName)
    SELECT cms.ServerName
    FROM config.ServerInventory cms
    WHERE cms.IsActive = 1
    AND (cms.ServerType = 'SQL Server' OR cms.ServerType IS NULL)
    AND NOT EXISTS (
        SELECT 1 FROM Patching.ServerPatchStatus sps 
        WHERE sps.ServerName = cms.ServerName
    );
    
    PRINT 'Synced ' + CAST(@@ROWCOUNT AS VARCHAR) + ' new servers from CMS (config.ServerInventory)';
END

GO

-- [Patching].[usp_TaniumAutomationReport]

CREATE PROCEDURE Patching.usp_TaniumAutomationReport
    @ShowNotConfiguredOnly BIT = 0
AS
BEGIN
    /*
    â•”â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•—
    â•‘ DBAOps Monitoring and Compliance Framework                                â•‘
    â•‘ Procedure: usp_TaniumAutomationReport                                     â•‘
    â•‘ Author: John Bosco RHOYLLE INNOVATION EMPIRE™, Sr SQL DBA/DBAOps Engineer                   â•‘
    â•šâ•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
    */
    SET NOCOUNT ON;
    
    PRINT 'â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•';
    PRINT '  TANIUM PATCHING AUTOMATION COMPLIANCE REPORT';
    PRINT '  DBAOps Monitoring and Compliance Framework';
    PRINT '  Generated: ' + CONVERT(VARCHAR(30), GETDATE(), 120);
    PRINT 'â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•';
    PRINT '';
    
    -- Summary
    SELECT 
        TaniumAutomationStatus,
        COUNT(*) AS ServerCount
    FROM Patching.vw_TaniumPatchingCompliance
    GROUP BY TaniumAutomationStatus
    ORDER BY 
        CASE TaniumAutomationStatus 
            WHEN 'NOT CONFIGURED' THEN 1 
            WHEN 'Unknown - Server Offline' THEN 2 
            ELSE 3 
        END;
    
    -- Environment breakdown
    SELECT 
        ServerEnvironment,
        SUM(CASE WHEN TaniumPatchingConfigured = 1 THEN 1 ELSE 0 END) AS Configured,
        SUM(CASE WHEN TaniumPatchingConfigured = 0 OR TaniumPatchingConfigured IS NULL THEN 1 ELSE 0 END) AS NotConfigured,
        COUNT(*) AS Total
    FROM Patching.TaniumPatchStatus
    WHERE IsActive = 1 AND TaniumAgentStatus != 'Offline'
    GROUP BY ServerEnvironment;
    
    -- Detailed list
    IF @ShowNotConfiguredOnly = 1
    BEGIN
        SELECT 
            ServerName,
            SQLVersion,
            ServerEnvironment,
            ExpectedTag,
            ExpectedTagCode,
            TaniumAutomationStatus,
            PatchComplianceStatus
        FROM Patching.vw_TaniumPatchingCompliance
        WHERE TaniumAutomationStatus = 'NOT CONFIGURED'
        ORDER BY ServerEnvironment, ServerName;
    END
    ELSE
    BEGIN
        SELECT 
            ServerName,
            SQLVersion,
            ServerEnvironment,
            TaniumPatchingTag,
            ExpectedTag,
            TaniumAutomationStatus,
            OverallComplianceStatus
        FROM Patching.vw_TaniumPatchingCompliance
        ORDER BY 
            CASE TaniumAutomationStatus WHEN 'NOT CONFIGURED' THEN 1 ELSE 2 END,
            ServerEnvironment, 
            ServerName;
    END
END

GO

-- [Patching].[usp_TestServerConnectivity]

CREATE   PROCEDURE [Patching].[usp_TestServerConnectivity]
    @ServerName NVARCHAR(128) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    -- Get server list from CMS instead of A2
    DECLARE @Servers TABLE (ServerName NVARCHAR(128));
    
    IF @ServerName IS NOT NULL
    BEGIN
        INSERT INTO @Servers (ServerName) VALUES (@ServerName);
    END
    ELSE
    BEGIN
        INSERT INTO @Servers (ServerName)
        SELECT ServerName 
        FROM config.ServerInventory 
        WHERE IsActive = 1 
        AND (ServerType = 'SQL Server' OR ServerType IS NULL);
    END
    
    -- Test connectivity for each server
    SELECT 
        s.ServerName,
        COALESCE(cms.Environment, cms.CMSEnvironment, 'Unknown') AS Environment,
        CASE 
            WHEN EXISTS (
                SELECT 1 FROM metrics.FACT_ServerHealth h 
                WHERE h.ServerName = s.ServerName 
                AND h.CaptureTime >= DATEADD(HOUR, -2, GETDATE())
            ) THEN 'Online'
            ELSE 'Offline/Unknown'
        END AS ConnectivityStatus,
        cms.LastSeen
    FROM @Servers s
    LEFT JOIN config.ServerInventory cms ON s.ServerName = cms.ServerName
    ORDER BY s.ServerName;
END

GO

-- [Patching].[usp_UpdateBuildReference]

CREATE PROCEDURE Patching.usp_UpdateBuildReference
    @SQLMajorVersion INT,
    @SQLVersionName NVARCHAR(50),
    @LatestCU NVARCHAR(20),
    @LatestBuildNumber NVARCHAR(20),
    @ReleaseDate DATE = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    IF EXISTS (SELECT 1 FROM Patching.SQLServerBuildReference WHERE SQLMajorVersion = @SQLMajorVersion)
    BEGIN
        UPDATE Patching.SQLServerBuildReference SET
            LatestCU = @LatestCU,
            LatestBuildNumber = @LatestBuildNumber,
            ReleaseDate = ISNULL(@ReleaseDate, ReleaseDate),
            LastUpdated = GETDATE()
        WHERE SQLMajorVersion = @SQLMajorVersion;
    END
    ELSE
    BEGIN
        INSERT INTO Patching.SQLServerBuildReference (SQLMajorVersion, SQLVersionName, LatestCU, LatestBuildNumber, ReleaseDate)
        VALUES (@SQLMajorVersion, @SQLVersionName, @LatestCU, @LatestBuildNumber, @ReleaseDate);
    END
END

GO

-- [Patching].[usp_UpdateComplianceStatus]

-- â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
-- Procedure: Patching.usp_UpdateComplianceStatus
-- Purpose: Calculate and update compliance status for all servers
-- Author: John Bosco RHOYLLE INNOVATION EMPIRE™
-- â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
CREATE PROCEDURE Patching.usp_UpdateComplianceStatus
AS
BEGIN
    /*
    â•”â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•—
    â•‘ DBAOps Monitoring and Compliance Framework                                â•‘
    â•‘ Procedure: usp_UpdateComplianceStatus                                     â•‘
    â•‘ Author: John Bosco RHOYLLE INNOVATION EMPIRE™, Sr SQL DBA/DBAOps Engineer                   â•‘
    â•šâ•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
    */
    SET NOCOUNT ON;
    
    DECLARE @MaxDays INT = 30;
    DECLARE @CriticalMaxDays INT = 14;
    
    SELECT @MaxDays = CAST(ConfigValue AS INT) FROM Patching.ComplianceConfiguration WHERE ConfigKey = 'MaxDaysSinceLastPatch';
    SELECT @CriticalMaxDays = CAST(ConfigValue AS INT) FROM Patching.ComplianceConfiguration WHERE ConfigKey = 'CriticalPatchMaxDays';
    
    -- Update days since last patch
    UPDATE Patching.TaniumPatchStatus
    SET DaysSinceLastPatch = DATEDIFF(DAY, LastPatchDate, GETDATE()),
        ModifiedDate = GETDATE()
    WHERE LastPatchDate IS NOT NULL;
    
    -- Update compliance status with proper logic
    UPDATE Patching.TaniumPatchStatus
    SET ComplianceStatus = 
        CASE 
            -- NO DATA = Unknown (not scanned yet)
            WHEN SQLVersion IS NULL OR CurrentBuildNumber IS NULL THEN 'Unknown'
            WHEN TaniumAgentStatus = 'Pending Scan' AND SQLVersion IS NULL THEN 'Unknown'
            WHEN LastPatchDate IS NULL AND SQLVersion IS NOT NULL THEN 'Unknown'
            -- Offline = Non-Compliant (can't verify)
            WHEN TaniumAgentStatus = 'Offline' THEN 'Non-Compliant'
            -- Non-compliant: Missing patches for more than 30 days
            WHEN DaysSinceLastPatch > @MaxDays THEN 'Non-Compliant'
            -- Non-compliant: Has missing critical patches beyond threshold
            WHEN MissingCriticalPatches > 0 AND DaysSinceLastPatch > @CriticalMaxDays THEN 'Non-Compliant'
            -- Warning: Approaching threshold
            WHEN DaysSinceLastPatch > (@MaxDays - 7) THEN 'Warning'
            -- Warning: Has missing patches
            WHEN MissingPatches > 0 THEN 'Warning'
            -- Compliant
            ELSE 'Compliant'
        END,
        ComplianceNotes = 
        CASE 
            WHEN SQLVersion IS NULL OR CurrentBuildNumber IS NULL THEN 'PENDING: Awaiting patch scan - run collector'
            WHEN TaniumAgentStatus = 'Pending Scan' AND SQLVersion IS NULL THEN 'PENDING: Server not yet scanned'
            WHEN LastPatchDate IS NULL AND SQLVersion IS NOT NULL THEN 'UNKNOWN: No patch date recorded'
            WHEN TaniumAgentStatus = 'Offline' THEN 'OFFLINE: Cannot verify compliance - server unreachable'
            WHEN DaysSinceLastPatch > @MaxDays THEN 'VIOLATION: ' + CAST(ISNULL(DaysSinceLastPatch,0) AS VARCHAR) + ' days since patch (CJIS/NIST max: ' + CAST(@MaxDays AS VARCHAR) + ')'
            WHEN MissingCriticalPatches > 0 AND DaysSinceLastPatch > @CriticalMaxDays THEN 'CRITICAL: ' + CAST(MissingCriticalPatches AS VARCHAR) + ' critical patches overdue'
            WHEN DaysSinceLastPatch > (@MaxDays - 7) THEN 'WARNING: ' + CAST(ISNULL(DaysSinceLastPatch,0) AS VARCHAR) + ' days - approaching ' + CAST(@MaxDays AS VARCHAR) + ' day limit'
            WHEN MissingPatches > 0 THEN 'INFO: ' + CAST(MissingPatches AS VARCHAR) + ' CU(s) behind - Latest: ' + ISNULL(LatestAvailableCU, 'N/A')
            ELSE 'COMPLIANT: Fully patched (' + ISNULL(CurrentCULevel, 'N/A') + ')'
        END,
        ModifiedDate = GETDATE()
    WHERE IsActive = 1;
    
    -- Archive to history (only servers with actual data)
    INSERT INTO Patching.TaniumPatchHistory (ServerName, SQLVersion, CurrentBuildNumber, CurrentCULevel, MissingPatches, ComplianceStatus, DaysSinceLastPatch)
    SELECT ServerName, SQLVersion, CurrentBuildNumber, CurrentCULevel, MissingPatches, ComplianceStatus, DaysSinceLastPatch
    FROM Patching.TaniumPatchStatus 
    WHERE IsActive = 1 AND SQLVersion IS NOT NULL;
    
    -- Return summary
    SELECT ComplianceStatus, COUNT(*) AS ServerCount
    FROM Patching.TaniumPatchStatus WHERE IsActive = 1
    GROUP BY ComplianceStatus
    ORDER BY CASE ComplianceStatus WHEN 'Non-Compliant' THEN 1 WHEN 'Warning' THEN 2 WHEN 'Unknown' THEN 3 ELSE 4 END;
END

GO

-- [Patching].[usp_UpdateMicrosoftRelease]

-- â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
-- Procedure: usp_UpdateMicrosoftRelease
-- Purpose: Add or update Microsoft release information
-- â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
CREATE PROCEDURE Patching.usp_UpdateMicrosoftRelease
    @SQLMajorVersion INT,
    @ReleaseName NVARCHAR(20),
    @KBNumber NVARCHAR(20),
    @BuildNumber NVARCHAR(20),
    @ReleaseDate DATE,
    @DownloadURL NVARCHAR(500) = NULL,
    @SecurityFixes INT = 0,
    @BugFixes INT = 0,
    @IsCritical BIT = 0,
    @ReleaseNotes NVARCHAR(MAX) = NULL
AS
BEGIN
    /*
    â•”â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•—
    â•‘ DBAOps Monitoring and Compliance Framework                                â•‘
    â•‘ Procedure: usp_UpdateMicrosoftRelease                                     â•‘
    â•‘ Author: John Bosco RHOYLLE INNOVATION EMPIRE™, Sr SQL DBA/DBAOps Engineer                   â•‘
    â•šâ•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
    */
    SET NOCOUNT ON;
    
    DECLARE @SQLVersionName NVARCHAR(50) = 
        CASE @SQLMajorVersion
            WHEN 17 THEN 'SQL Server 2025'
            WHEN 16 THEN 'SQL Server 2022'
            WHEN 15 THEN 'SQL Server 2019'
            WHEN 14 THEN 'SQL Server 2017'
            WHEN 13 THEN 'SQL Server 2016'
            ELSE 'Unknown'
        END;
    
    DECLARE @ReleaseType NVARCHAR(20) = 
        CASE 
            WHEN @ReleaseName LIKE 'CU%' THEN 'CU'
            WHEN @ReleaseName LIKE 'SP%' THEN 'SP'
            WHEN @ReleaseName LIKE 'GDR%' THEN 'GDR'
            ELSE 'Other'
        END;
    
    -- Mark previous releases as not latest
    UPDATE Patching.MicrosoftReleases 
    SET IsLatest = 0 
    WHERE SQLMajorVersion = @SQLMajorVersion AND ReleaseType = @ReleaseType;
    
    -- Insert or update
    IF EXISTS (SELECT 1 FROM Patching.MicrosoftReleases WHERE SQLMajorVersion = @SQLMajorVersion AND KBNumber = @KBNumber)
    BEGIN
        UPDATE Patching.MicrosoftReleases
        SET ReleaseName = @ReleaseName,
            BuildNumber = @BuildNumber,
            ReleaseDate = @ReleaseDate,
            DownloadURL = @DownloadURL,
            SecurityFixes = @SecurityFixes,
            BugFixes = @BugFixes,
            IsCritical = @IsCritical,
            ReleaseNotes = @ReleaseNotes,
            IsLatest = 1
        WHERE SQLMajorVersion = @SQLMajorVersion AND KBNumber = @KBNumber;
        
        PRINT 'Updated release: ' + @SQLVersionName + ' ' + @ReleaseName + ' (' + @KBNumber + ')';
    END
    ELSE
    BEGIN
        INSERT INTO Patching.MicrosoftReleases 
        (SQLMajorVersion, SQLVersionName, ReleaseType, ReleaseName, KBNumber, BuildNumber, ReleaseDate, DownloadURL, SecurityFixes, BugFixes, IsCritical, IsLatest, ReleaseNotes)
        VALUES 
        (@SQLMajorVersion, @SQLVersionName, @ReleaseType, @ReleaseName, @KBNumber, @BuildNumber, @ReleaseDate, @DownloadURL, @SecurityFixes, @BugFixes, @IsCritical, 1, @ReleaseNotes);
        
        PRINT 'Added new release: ' + @SQLVersionName + ' ' + @ReleaseName + ' (' + @KBNumber + ')';
    END
    
    -- Update SQLServerBuildReference table
    UPDATE Patching.SQLServerBuildReference
    SET LatestCU = @ReleaseName,
        LatestBuildNumber = @BuildNumber,
        KBNumber = @KBNumber,
        DownloadURL = @DownloadURL,
        ReleaseDate = @ReleaseDate,
        LastUpdated = GETDATE()
    WHERE SQLMajorVersion = @SQLMajorVersion;
END

GO

-- [Patching].[usp_UpdatePatchResult]

-- ============================================================
-- 4. PROCEDURE: Update patch execution result
-- Called after PowerShell completes
-- ============================================================
CREATE   PROCEDURE Patching.usp_UpdatePatchResult
    @QueueID INT,
    @ExitCode INT,
    @ResultMessage VARCHAR(MAX) = NULL,
    @LogFilePath VARCHAR(500) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @Status VARCHAR(50) = CASE 
        WHEN @ExitCode = 0 THEN 'Completed'
        WHEN @ExitCode = 3010 THEN 'Completed-RebootRequired'
        ELSE 'Failed'
    END;
    
    DECLARE @ScheduleID INT;
    
    -- Update queue record
    UPDATE Patching.PatchExecutionQueue
    SET QueueStatus = @Status,
        CompletedDate = GETDATE(),
        ExitCode = @ExitCode,
        ResultMessage = @ResultMessage,
        LogFilePath = @LogFilePath
    WHERE QueueID = @QueueID;
    
    -- Get linked schedule
    SELECT @ScheduleID = ScheduleID FROM Patching.PatchExecutionQueue WHERE QueueID = @QueueID;
    
    -- Update schedule if linked
    IF @ScheduleID IS NOT NULL
    BEGIN
        UPDATE Patching.ScheduledPatches
        SET Status = CASE WHEN @ExitCode IN (0, 3010) THEN 'Completed' ELSE 'Failed' END,
            ActualEndTime = GETDATE(),
            ExecutionLog = @ResultMessage,
            ErrorMessage = CASE WHEN @ExitCode NOT IN (0, 3010) THEN @ResultMessage ELSE NULL END,
            ModifiedDate = GETDATE()
        WHERE ScheduleID = @ScheduleID;
    END
    
    PRINT 'Patch result updated: QueueID=' + CAST(@QueueID AS VARCHAR) + ', Status=' + @Status;
END

GO

-- [Patching].[usp_UpdatePatchStatus]

CREATE PROCEDURE Patching.usp_UpdatePatchStatus
    @ScheduleID INT,
    @Status NVARCHAR(50),
    @PostPatchScore INT = NULL,
    @Notes NVARCHAR(1000) = NULL,
    @UpdatedBy NVARCHAR(100) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @ServerName NVARCHAR(128), @PatchType NVARCHAR(50), @KBNumber NVARCHAR(20), @TargetBuild NVARCHAR(50);
    
    SELECT @ServerName = ServerName, @PatchType = PatchType, @KBNumber = KBNumber, @TargetBuild = TargetBuild
    FROM Patching.PatchSchedule WHERE ScheduleID = @ScheduleID;
    
    UPDATE Patching.PatchSchedule
    SET Status = @Status,
        ExecutionLog = ISNULL(ExecutionLog, '') + CHAR(13) + CHAR(10) + 
                       CONVERT(VARCHAR, GETDATE(), 120) + ' [' + @Status + '] ' + ISNULL(@Notes, ''),
        PostPatchScore = ISNULL(@PostPatchScore, PostPatchScore),
        CompletedDate = CASE WHEN @Status IN ('Completed', 'Failed', 'Cancelled') THEN GETDATE() ELSE CompletedDate END
    WHERE ScheduleID = @ScheduleID;
    
    IF @Status = 'Completed'
    BEGIN
        DECLARE @BuildBefore NVARCHAR(50);
        SELECT @BuildBefore = CurrentBuild FROM Patching.PatchSchedule WHERE ScheduleID = @ScheduleID;
        
        INSERT INTO Patching.PatchHistory (ServerName, PatchDate, PatchType, KBNumber, BuildBefore, BuildAfter, Status, ExecutedBy)
        VALUES (@ServerName, GETDATE(), @PatchType, @KBNumber, @BuildBefore, @TargetBuild, 'Success', @UpdatedBy);
        
        UPDATE config.ServerInventory SET SQLBuild = @TargetBuild WHERE ServerName = @ServerName;
    END
    
    SELECT @ScheduleID AS ScheduleID, @Status AS Status, 'Updated' AS Action;
END

GO

-- [Patching].[usp_UpdateServerPatchInfo]

-- â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
-- Procedure: Patching.usp_UpdateServerPatchInfo
-- Purpose: Update patch info for a single server (called by PowerShell)
-- Author: John Bosco RHOYLLE INNOVATION EMPIRE™
-- â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
CREATE PROCEDURE Patching.usp_UpdateServerPatchInfo
    @ServerName NVARCHAR(128),
    @SQLVersion NVARCHAR(256) = NULL,
    @SQLEdition NVARCHAR(128) = NULL,
    @CurrentBuildNumber NVARCHAR(50) = NULL,
    @CurrentCULevel NVARCHAR(50) = NULL,
    @MissingPatches INT = 0,
    @MissingCriticalPatches INT = 0,
    @LatestAvailableCU NVARCHAR(50) = NULL,
    @LatestAvailableBuild NVARCHAR(50) = NULL,
    @TaniumAgentStatus NVARCHAR(50) = 'Online'
AS
BEGIN
    /*
    â•”â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•—
    â•‘ DBAOps Monitoring and Compliance Framework                                â•‘
    â•‘ Procedure: usp_UpdateServerPatchInfo                                      â•‘
    â•‘ Author: John Bosco RHOYLLE INNOVATION EMPIRE™, Sr SQL DBA/DBAOps Engineer                   â•‘
    â•šâ•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
    */
    SET NOCOUNT ON;
    
    IF EXISTS (SELECT 1 FROM Patching.TaniumPatchStatus WHERE ServerName = @ServerName)
    BEGIN
        UPDATE Patching.TaniumPatchStatus SET
            SQLVersion = ISNULL(@SQLVersion, SQLVersion),
            SQLEdition = ISNULL(@SQLEdition, SQLEdition),
            CurrentBuildNumber = ISNULL(@CurrentBuildNumber, CurrentBuildNumber),
            CurrentCULevel = ISNULL(@CurrentCULevel, CurrentCULevel),
            MissingPatches = @MissingPatches,
            MissingCriticalPatches = @MissingCriticalPatches,
            LatestAvailableCU = ISNULL(@LatestAvailableCU, LatestAvailableCU),
            LatestAvailableBuild = ISNULL(@LatestAvailableBuild, LatestAvailableBuild),
            TaniumAgentStatus = @TaniumAgentStatus,
            TaniumLastScanDate = GETDATE(),
            LastPatchDate = CASE 
                WHEN LastPatchDate IS NULL AND @TaniumAgentStatus = 'Online' AND @CurrentBuildNumber IS NOT NULL 
                THEN GETDATE() 
                ELSE LastPatchDate 
            END,
            ModifiedDate = GETDATE()
        WHERE ServerName = @ServerName;
    END
    ELSE
    BEGIN
        INSERT INTO Patching.TaniumPatchStatus 
        (ServerName, SQLVersion, SQLEdition, CurrentBuildNumber, CurrentCULevel, MissingPatches, MissingCriticalPatches, LatestAvailableCU, LatestAvailableBuild, TaniumAgentStatus, TaniumLastScanDate, LastPatchDate, IsActive)
        VALUES 
        (@ServerName, @SQLVersion, @SQLEdition, @CurrentBuildNumber, @CurrentCULevel, @MissingPatches, @MissingCriticalPatches, @LatestAvailableCU, @LatestAvailableBuild, @TaniumAgentStatus, GETDATE(), 
         CASE WHEN @TaniumAgentStatus = 'Online' AND @CurrentBuildNumber IS NOT NULL THEN GETDATE() ELSE NULL END, 1);
    END
END

GO

-- [Patching].[usp_UpdateTaniumCompliance]

-- â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
-- Procedure: usp_UpdateTaniumCompliance
-- Purpose: Calculate expected tags, compliance status, and propose solutions
-- â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
CREATE PROCEDURE Patching.usp_UpdateTaniumCompliance
AS
BEGIN
    /*
    â•”â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•—
    â•‘ DBAOps Monitoring and Compliance Framework                                â•‘
    â•‘ Procedure: usp_UpdateTaniumCompliance                                     â•‘
    â•‘ Author: John Bosco RHOYLLE INNOVATION EMPIRE™, Sr SQL DBA/DBAOps Engineer                   â•‘
    â•šâ•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
    */
    SET NOCOUNT ON;
    
    DECLARE @MaxDays INT = 30;
    SELECT @MaxDays = CAST(ConfigValue AS INT) FROM Patching.ComplianceConfiguration WHERE ConfigKey = 'MaxDaysSinceLastPatch';
    
    -- Update expected Tanium tag based on environment and replication role
    UPDATE Patching.TaniumPatchStatus
    SET ExpectedTaniumTag = 
        CASE 
            WHEN IsReplicationSubscriber = 1 THEN 'SERVER_SQLSERVER_SUBSCRIBER'
            WHEN IsReplicationPublisher = 1 THEN 'SERVER_SQLSERVER_PUBLISHER'
            WHEN ServerEnvironment = 'Non-Prod' THEN 'SERVER_SQLSERVER_NON-PROD'
            ELSE 'SERVER_SQLSERVER_PRODUCTION'
        END
    WHERE IsActive = 1;
    
    -- Check for tag mismatch
    UPDATE Patching.TaniumPatchStatus
    SET TaniumTagMismatch = 
        CASE 
            WHEN TaniumPatchingTag IS NOT NULL AND TaniumPatchingTag != ExpectedTaniumTag THEN 1
            ELSE 0
        END
    WHERE IsActive = 1;
    
    -- Update days since last patch
    UPDATE Patching.TaniumPatchStatus
    SET DaysSinceLastPatch = DATEDIFF(DAY, LastPatchDate, GETDATE())
    WHERE LastPatchDate IS NOT NULL;
    
    -- Update compliance status
    UPDATE Patching.TaniumPatchStatus
    SET ComplianceStatus = 
        CASE 
            WHEN TaniumAgentStatus = 'Offline' THEN 'Unknown'
            WHEN SQLVersion IS NULL THEN 'Unknown'
            WHEN CurrentCULevel = 'RTM' AND MissingPatches > 0 THEN 'Non-Compliant'
            WHEN DaysSinceLastPatch > @MaxDays THEN 'Non-Compliant'
            WHEN MissingPatches >= 3 THEN 'Warning'
            WHEN MissingPatches > 0 THEN 'Warning'
            ELSE 'Compliant'
        END
    WHERE IsActive = 1;
    
    -- Update non-compliance reasons and proposed solutions
    UPDATE Patching.TaniumPatchStatus
    SET NonComplianceReason = 
        CASE 
            -- Offline servers
            WHEN TaniumAgentStatus = 'Offline' THEN 
                'Server is offline or unreachable'
            -- No Tanium tag
            WHEN TaniumPatchingConfigured = 0 OR TaniumPatchingConfigured IS NULL THEN 
                'Tanium patching automation NOT configured - Missing registry tag'
            -- Wrong tag
            WHEN TaniumTagMismatch = 1 THEN 
                'Tanium tag mismatch - Current: ' + ISNULL(TaniumPatchingTag,'NULL') + ', Expected: ' + ISNULL(ExpectedTaniumTag,'NULL')
            -- Behind on patches
            WHEN CurrentCULevel = 'RTM' THEN 
                'Server at RTM with no Cumulative Updates installed - ' + CAST(ISNULL(MissingPatches,0) AS VARCHAR) + ' CUs behind'
            WHEN MissingPatches > 0 THEN 
                'Behind on patches - ' + CAST(MissingPatches AS VARCHAR) + ' CU(s) behind latest (' + ISNULL(LatestAvailableCU,'N/A') + ')'
            WHEN DaysSinceLastPatch > @MaxDays THEN 
                'Patch age exceeds ' + CAST(@MaxDays AS VARCHAR) + ' days - Last patched ' + CAST(ISNULL(DaysSinceLastPatch,0) AS VARCHAR) + ' days ago'
            ELSE NULL
        END,
        ProposedSolution = 
        CASE 
            -- Offline servers
            WHEN TaniumAgentStatus = 'Offline' THEN 
                'Verify server status, check network connectivity, confirm server is powered on'
            -- No Tanium tag
            WHEN TaniumPatchingConfigured = 0 OR TaniumPatchingConfigured IS NULL THEN 
                'Run Tanium tagging script on server with code ' + 
                CASE 
                    WHEN ServerEnvironment = 'Non-Prod' THEN '3 (Non-Prod)'
                    ELSE '4 (Production)'
                END + ' to add tag: ' + ISNULL(ExpectedTaniumTag,'SERVER_SQLSERVER_PRODUCTION')
            -- Wrong tag
            WHEN TaniumTagMismatch = 1 THEN 
                'Remove current tag and re-run Tanium tagging script with correct code for ' + ISNULL(ExpectedTaniumTag,'')
            -- Behind on patches
            WHEN CurrentCULevel = 'RTM' OR MissingPatches > 0 THEN 
                'Schedule maintenance window to install ' + ISNULL(LatestAvailableCU,'latest CU') + '. Verify Tanium automation is working.'
            WHEN DaysSinceLastPatch > @MaxDays THEN 
                'Investigate why patches are not being applied. Check Tanium client status and patch deployment schedule.'
            ELSE NULL
        END
    WHERE IsActive = 1;
    
    PRINT 'Tanium compliance status updated.';
END

GO

-- [Patching].[usp_UpdateTaniumTagStatus]

CREATE PROCEDURE Patching.usp_UpdateTaniumTagStatus
    @ServerName NVARCHAR(128),
    @TaniumPatchingTag NVARCHAR(100) = NULL,
    @TaniumPatchingConfigured BIT = 0,
    @IsReplicationPublisher BIT = 0,
    @IsReplicationSubscriber BIT = 0
AS
BEGIN
    /*
    â•”â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•—
    â•‘ DBAOps Monitoring and Compliance Framework                                â•‘
    â•‘ Procedure: usp_UpdateTaniumTagStatus                                      â•‘
    â•‘ Author: John Bosco RHOYLLE INNOVATION EMPIRE™, Sr SQL DBA/DBAOps Engineer                   â•‘
    â•šâ•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
    */
    SET NOCOUNT ON;
    
    UPDATE Patching.TaniumPatchStatus
    SET TaniumPatchingTag = @TaniumPatchingTag,
        TaniumPatchingConfigured = @TaniumPatchingConfigured,
        IsReplicationPublisher = @IsReplicationPublisher,
        IsReplicationSubscriber = @IsReplicationSubscriber,
        TaniumTagLastChecked = GETDATE(),
        ModifiedDate = GETDATE()
    WHERE ServerName = @ServerName;
    
    PRINT 'Updated Tanium tag status for: ' + @ServerName;
END

GO

-- [Patching].[usp_VerifyTaniumConfiguration]

-- â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
-- Procedure: Patching.usp_VerifyTaniumConfiguration
-- Purpose: Verify system configuration
-- Author: John Bosco RHOYLLE INNOVATION EMPIRE™
-- â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
CREATE PROCEDURE Patching.usp_VerifyTaniumConfiguration
AS
BEGIN
    /*
    â•”â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•—
    â•‘ DBAOps Monitoring and Compliance Framework                                â•‘
    â•‘ Procedure: usp_VerifyTaniumConfiguration                                  â•‘
    â•‘ Author: John Bosco RHOYLLE INNOVATION EMPIRE™, Sr SQL DBA/DBAOps Engineer                   â•‘
    â•šâ•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
    */
    SET NOCOUNT ON;
    
    DECLARE @FrameworkName NVARCHAR(100), @FrameworkAuthor NVARCHAR(200);
    SELECT @FrameworkName = ConfigValue FROM Patching.ComplianceConfiguration WHERE ConfigKey = 'FrameworkName';
    SELECT @FrameworkAuthor = ConfigValue FROM Patching.ComplianceConfiguration WHERE ConfigKey = 'FrameworkAuthor';
    
    PRINT 'â•”â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•—';
    PRINT 'â•‘ ' + ISNULL(@FrameworkName, 'DBAOps Monitoring and Compliance Framework');
    PRINT 'â•‘ Configuration Verification Report';
    PRINT 'â•‘ Author: ' + ISNULL(@FrameworkAuthor, 'DBA Team');
    PRINT 'â• â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•£';
    PRINT 'â•‘ Executed: ' + CONVERT(VARCHAR(30), GETDATE(), 120);
    PRINT 'â•šâ•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•';
    PRINT '';
    
    -- Check results
    DECLARE @Results TABLE (Category NVARCHAR(50), Item NVARCHAR(100), Status NVARCHAR(20), Details NVARCHAR(500));
    
    INSERT INTO @Results VALUES ('Schema', 'Patching Schema', 
        CASE WHEN EXISTS (SELECT 1 FROM sys.schemas WHERE name = 'Patching') THEN 'PASS' ELSE 'FAIL' END, 'Required schema');
    
    INSERT INTO @Results VALUES ('Tables', 'TaniumPatchStatus', 
        CASE WHEN OBJECT_ID('Patching.TaniumPatchStatus') IS NOT NULL THEN 'PASS' ELSE 'FAIL' END,
        'Records: ' + CAST((SELECT COUNT(*) FROM Patching.TaniumPatchStatus) AS VARCHAR));
    
    INSERT INTO @Results VALUES ('Tables', 'SQLServerBuildReference', 
        CASE WHEN OBJECT_ID('Patching.SQLServerBuildReference') IS NOT NULL THEN 'PASS' ELSE 'FAIL' END,
        'Build versions defined');
    
    INSERT INTO @Results VALUES ('Email', 'Database Mail', 
        CASE WHEN EXISTS (SELECT 1 FROM msdb.dbo.sysmail_profile) THEN 'PASS' ELSE 'FAIL' END, 'Required for reports');
    
    INSERT INTO @Results 
    SELECT 'Data', 'Server Scan Status',
        CASE WHEN SUM(CASE WHEN SQLVersion IS NOT NULL THEN 1 ELSE 0 END) > 0 THEN 'PASS' ELSE 'WARNING' END,
        CAST(SUM(CASE WHEN SQLVersion IS NOT NULL THEN 1 ELSE 0 END) AS VARCHAR) + ' of ' + CAST(COUNT(*) AS VARCHAR) + ' servers scanned'
    FROM Patching.TaniumPatchStatus WHERE IsActive = 1;
    
    SELECT Category, Item, Status, Details FROM @Results
    ORDER BY CASE Status WHEN 'FAIL' THEN 1 WHEN 'WARNING' THEN 2 ELSE 3 END, Category;
    
    -- Compliance summary
    PRINT '';
    PRINT 'Compliance Status Summary:';
    SELECT ComplianceStatus, COUNT(*) AS Count FROM Patching.TaniumPatchStatus WHERE IsActive = 1 GROUP BY ComplianceStatus;
END

GO

-- [Patching].[usp_ViewStatus]

-- â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
-- SECTION 20: PROCEDURE - VIEW STATUS
-- â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
CREATE   PROCEDURE Patching.usp_ViewStatus
    @ShowQueue BIT = 1,
    @ShowHistory BIT = 1,
    @Days INT = 7
AS
BEGIN
    SET NOCOUNT ON;
    
    IF @ShowQueue = 1
    BEGIN
        PRINT 'â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•';
        PRINT '  DEPLOYMENT QUEUE';
        PRINT 'â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•';
        
        SELECT ServerName, Environment, TargetCU, KBNumber, Status, ScheduledDate, RequestedBy
        FROM Patching.vw_DeploymentQueue
        WHERE Status IN ('Pending', 'Approved', 'InProgress')
        ORDER BY Status, Priority;
    END
    
    IF @ShowHistory = 1
    BEGIN
        PRINT '';
        PRINT 'â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•';
        PRINT '  DEPLOYMENT HISTORY (Last ' + CAST(@Days AS VARCHAR) + ' days)';
        PRINT 'â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•';
        
        SELECT ServerName, Environment, PreDeployCU AS [Before], PostDeployCU AS [After],
               DeploymentStatus AS Status, DurationMinutes AS [Duration],
               CASE WHEN RebootRequired = 1 THEN 'Yes' ELSE 'No' END AS Reboot,
               DeploymentStartTime, DeployedBy
        FROM Patching.vw_DeploymentHistory
        WHERE DeploymentStartTime >= DATEADD(DAY, -@Days, GETDATE())
        ORDER BY DeploymentStartTime DESC;
    END
END;

GO

-- [pbi].[usp_RefreshDateDimension]

CREATE PROCEDURE pbi.usp_RefreshDateDimension
AS
BEGIN
    SET NOCOUNT ON;
    
    -- Update current day/week/month flags
    UPDATE pbi.DimDate
    SET 
        IsCurrentDay = CASE WHEN FullDate = CAST(GETDATE() AS DATE) THEN 1 ELSE 0 END,
        IsCurrentWeek = CASE WHEN DATEPART(WEEK, FullDate) = DATEPART(WEEK, GETDATE()) 
                             AND YEAR(FullDate) = YEAR(GETDATE()) THEN 1 ELSE 0 END,
        IsCurrentMonth = CASE WHEN MONTH(FullDate) = MONTH(GETDATE()) 
                              AND YEAR(FullDate) = YEAR(GETDATE()) THEN 1 ELSE 0 END,
        IsCurrentYear = CASE WHEN YEAR(FullDate) = YEAR(GETDATE()) THEN 1 ELSE 0 END,
        RelativeDayOffset = DATEDIFF(DAY, CAST(GETDATE() AS DATE), FullDate),
        RelativeMonthOffset = DATEDIFF(MONTH, CAST(GETDATE() AS DATE), FullDate);
    
    PRINT 'Date Dimension flags refreshed';
END

GO

-- [perf].[usp_CollectIndexUsage]
CREATE OR ALTER PROCEDURE perf.usp_CollectIndexUsage
AS
BEGIN
    SET NOCOUNT ON;
    DECLARE @ServerName NVARCHAR(128) = @@SERVERNAME;
    
    INSERT INTO perf.IndexUsageStats (ServerName, DatabaseName, TableName, IndexName, UserSeeks, UserScans, UserLookups, UserUpdates, SizeKB)
    SELECT @ServerName, DB_NAME(), OBJECT_NAME(i.object_id), i.name,
        ISNULL(ius.user_seeks, 0), ISNULL(ius.user_scans, 0), ISNULL(ius.user_lookups, 0), ISNULL(ius.user_updates, 0),
        (SELECT SUM(used_page_count) * 8 FROM sys.dm_db_partition_stats ps WHERE ps.object_id = i.object_id AND ps.index_id = i.index_id)
    FROM sys.indexes i
    LEFT JOIN sys.dm_db_index_usage_stats ius ON i.object_id = ius.object_id AND i.index_id = ius.index_id AND ius.database_id = DB_ID()
    WHERE OBJECTPROPERTY(i.object_id, 'IsUserTable') = 1 AND i.name IS NOT NULL;
END

GO

-- [perf].[usp_CollectMissingIndexes]
CREATE OR ALTER PROCEDURE perf.usp_CollectMissingIndexes
AS
BEGIN
    SET NOCOUNT ON;
    DECLARE @ServerName NVARCHAR(128) = @@SERVERNAME;
    
    INSERT INTO perf.MissingIndexes (ServerName, DatabaseName, TableName, EqualityColumns, InequalityColumns, IncludedColumns, ImprovementMeasure, CreateIndexStatement)
    SELECT @ServerName, DB_NAME(mid.database_id), mid.statement, mid.equality_columns, mid.inequality_columns, mid.included_columns,
        migs.avg_total_user_cost * migs.avg_user_impact * (migs.user_seeks + migs.user_scans),
        'CREATE INDEX IX_Missing_' + CAST(mid.index_handle AS VARCHAR) + ' ON ' + mid.statement + ' (' + ISNULL(mid.equality_columns,'') + CASE WHEN mid.equality_columns IS NOT NULL AND mid.inequality_columns IS NOT NULL THEN ',' ELSE '' END + ISNULL(mid.inequality_columns,'') + ')' + CASE WHEN mid.included_columns IS NOT NULL THEN ' INCLUDE (' + mid.included_columns + ')' ELSE '' END
    FROM sys.dm_db_missing_index_details mid
    INNER JOIN sys.dm_db_missing_index_groups mig ON mid.index_handle = mig.index_handle
    INNER JOIN sys.dm_db_missing_index_group_stats migs ON mig.index_group_handle = migs.group_handle
    WHERE migs.avg_total_user_cost * migs.avg_user_impact * (migs.user_seeks + migs.user_scans) > 1000
    ORDER BY migs.avg_total_user_cost * migs.avg_user_impact * (migs.user_seeks + migs.user_scans) DESC;
END

GO

-- [perf].[usp_CollectTopQueries]

CREATE PROCEDURE perf.usp_CollectTopQueries 
    @TopN INT = 50
AS
BEGIN
    SET NOCOUNT ON;
    DECLARE @ServerName NVARCHAR(128) = @@SERVERNAME;
    
    INSERT INTO perf.TopQueries (ServerName, DatabaseName, QueryHash, QueryText, ExecutionCount, TotalWorkerTimeMs, TotalElapsedTimeMs, TotalLogicalReads, RankByCPU, RankByReads)
    SELECT TOP (@TopN) @ServerName, ISNULL(DB_NAME(qt.dbid), 'Unknown'), qs.query_hash,
        SUBSTRING(qt.text, (qs.statement_start_offset/2)+1, ((CASE qs.statement_end_offset WHEN -1 THEN DATALENGTH(qt.text) ELSE qs.statement_end_offset END - qs.statement_start_offset)/2)+1),
        qs.execution_count, qs.total_worker_time/1000, qs.total_elapsed_time/1000, qs.total_logical_reads,
        ROW_NUMBER() OVER (ORDER BY qs.total_worker_time DESC), ROW_NUMBER() OVER (ORDER BY qs.total_logical_reads DESC)
    FROM sys.dm_exec_query_stats qs CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) qt
    WHERE qs.execution_count > 0 ORDER BY qs.total_worker_time DESC;
END

GO

-- [perf].[usp_CollectWaitStatistics]
CREATE OR ALTER PROCEDURE perf.usp_CollectWaitStatistics
AS
BEGIN
    SET NOCOUNT ON;
    DECLARE @ServerName NVARCHAR(128) = @@SERVERNAME;
    
    INSERT INTO perf.WaitStatistics (ServerName, WaitType, WaitCategory, WaitingTasksCount, WaitTimeMs, SignalWaitTimeMs)
    SELECT @ServerName, ws.wait_type, COALESCE(wr.WaitCategory, 'Other'), ws.waiting_tasks_count, ws.wait_time_ms, ws.signal_wait_time_ms
    FROM sys.dm_os_wait_stats ws
    LEFT JOIN perf.WaitTypeReference wr ON ws.wait_type = wr.WaitType
    WHERE ws.wait_time_ms > 0
    AND ws.wait_type NOT IN ('CLR_SEMAPHORE','LAZYWRITER_SLEEP','RESOURCE_QUEUE','SLEEP_TASK','SLEEP_SYSTEMTASK','SQLTRACE_BUFFER_FLUSH','WAITFOR','LOGMGR_QUEUE','CHECKPOINT_QUEUE','REQUEST_FOR_DEADLOCK_SEARCH','XE_TIMER_EVENT','BROKER_TO_FLUSH','BROKER_TASK_STOP','CLR_MANUAL_EVENT','DISPATCHER_QUEUE_SEMAPHORE','FT_IFTS_SCHEDULER_IDLE_WAIT','XE_DISPATCHER_WAIT','HADR_FILESTREAM_IOMGR_IOCOMPLETION')
    ORDER BY ws.wait_time_ms DESC;
END

GO

-- [playbook].[usp_CompleteStep]

-- =============================================================================
-- PROCEDURE: playbook.usp_CompleteStep
-- =============================================================================
CREATE   PROCEDURE playbook.usp_CompleteStep
    @ExecutionID BIGINT,
    @StepNumber INT,
    @Status NVARCHAR(50),
    @Output NVARCHAR(MAX) = NULL,
    @ErrorMessage NVARCHAR(MAX) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    -- Update step execution
    UPDATE playbook.StepExecution
    SET Status = @Status,
        CompletedDate = GETDATE(),
        Output = @Output,
        ErrorMessage = @ErrorMessage
    WHERE ExecutionID = @ExecutionID AND StepNumber = @StepNumber;
    
    -- Get next step number
    DECLARE @NextStep INT;
    SELECT @NextStep = MIN(StepNumber)
    FROM playbook.StepExecution
    WHERE ExecutionID = @ExecutionID AND Status = 'Pending';
    
    IF @NextStep IS NOT NULL
    BEGIN
        UPDATE playbook.ExecutionHistory
        SET CurrentStepNumber = @NextStep
        WHERE ExecutionID = @ExecutionID;
    END
    ELSE
    BEGIN
        -- All steps complete
        UPDATE playbook.ExecutionHistory
        SET Status = CASE WHEN @Status = 'Failed' THEN 'Failed' ELSE 'Completed' END,
            CompletedDate = GETDATE(),
            ResultSummary = CONCAT('Completed ', @StepNumber, ' steps')
        WHERE ExecutionID = @ExecutionID;
    END
    
    SELECT @NextStep AS NextStepNumber, 
           CASE WHEN @NextStep IS NULL THEN 'Playbook Complete' ELSE 'Continue' END AS Status;
END

GO

-- [playbook].[usp_GetNextStep]

-- =============================================================================
-- PROCEDURE: playbook.usp_GetNextStep
-- =============================================================================
CREATE   PROCEDURE playbook.usp_GetNextStep
    @ExecutionID BIGINT
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @CurrentStep INT;
    SELECT @CurrentStep = CurrentStepNumber FROM playbook.ExecutionHistory WHERE ExecutionID = @ExecutionID;
    
    SELECT 
        s.StepID,
        s.StepNumber,
        s.StepTitle,
        s.StepDescription,
        s.StepType,
        r.RiskName,
        r.Color AS RiskColor,
        r.RequiresApproval,
        r.RequiresCheckpoint,
        s.ScriptType,
        s.Script,
        s.ExpectedOutcome,
        s.RollbackScript,
        s.TimeoutSeconds,
        s.RequiresConfirmation,
        s.ConfirmationPrompt,
        se.Status AS CurrentStatus
    FROM playbook.Steps s
    JOIN playbook.RiskLevels r ON s.RiskLevel = r.RiskLevelID
    JOIN playbook.StepExecution se ON s.StepID = se.StepID AND se.ExecutionID = @ExecutionID
    WHERE s.StepNumber = @CurrentStep
      AND s.PlaybookID = (SELECT PlaybookID FROM playbook.ExecutionHistory WHERE ExecutionID = @ExecutionID);
END

GO

-- [playbook].[usp_GetPlaybookForAlert]

-- =============================================================================
-- PROCEDURE: playbook.usp_GetPlaybookForAlert
-- =============================================================================
CREATE   PROCEDURE playbook.usp_GetPlaybookForAlert
    @AlertType NVARCHAR(100),
    @ServerName NVARCHAR(128) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    SELECT 
        p.PlaybookID,
        p.PlaybookCode,
        p.PlaybookName,
        p.Description,
        c.CategoryName,
        r.RiskName,
        r.Color AS RiskColor,
        p.EstimatedMinutes,
        p.RequiresCJISApproval,
        playbook.fn_CanExecuteOnServer(p.PlaybookID, @ServerName) AS CanExecuteOnServer,
        (SELECT COUNT(*) FROM playbook.Steps WHERE PlaybookID = p.PlaybookID) AS TotalSteps
    FROM playbook.Playbooks p
    JOIN playbook.Categories c ON p.CategoryID = c.CategoryID
    JOIN playbook.RiskLevels r ON p.OverallRiskLevel = r.RiskLevelID
    WHERE p.IsActive = 1
      AND (p.TriggerAlertTypes LIKE '%' + @AlertType + '%' OR p.TriggerAlertTypes IS NULL)
    ORDER BY r.RiskLevelID, p.PlaybookName;
END

GO

-- [playbook].[usp_StartExecution]

-- =============================================================================
-- PROCEDURE: playbook.usp_StartExecution
-- =============================================================================
CREATE   PROCEDURE playbook.usp_StartExecution
    @PlaybookID INT,
    @ServerName NVARCHAR(128),
    @DatabaseName NVARCHAR(128) = NULL,
    @IncidentID BIGINT = NULL,
    @ExecutedBy NVARCHAR(100) = NULL,
    @TrackITTicket NVARCHAR(50) = NULL,
    @ExecutionID BIGINT OUTPUT
AS
BEGIN
    SET NOCOUNT ON;
    
    -- Validate server exists in CMS-synced inventory
    IF NOT EXISTS (SELECT 1 FROM config.ServerInventory WHERE ServerName = @ServerName AND IsActive = 1)
    BEGIN
        RAISERROR('Server not found in CMS-synced inventory: %s', 16, 1, @ServerName);
        RETURN;
    END
    
    -- Check if playbook can execute on this server
    IF playbook.fn_CanExecuteOnServer(@PlaybookID, @ServerName) = 0
    BEGIN
        DECLARE @CMSGroup NVARCHAR(200) = correlation.fn_GetCMSGroup(@ServerName);
        RAISERROR('Playbook not allowed on server %s (CMS Group: %s). Check risk level and group restrictions.', 16, 1, @ServerName, @CMSGroup);
        RETURN;
    END
    
    -- Get CMS group for logging
    DECLARE @CMSGroupName NVARCHAR(200) = correlation.fn_GetCMSGroup(@ServerName);
    
    -- Create execution record
    INSERT INTO playbook.ExecutionHistory (
        PlaybookID, IncidentID, ServerName, CMSGroupName, DatabaseName,
        ExecutedBy, TrackITTicket, Status, CurrentStepNumber
    )
    VALUES (
        @PlaybookID, @IncidentID, @ServerName, @CMSGroupName, @DatabaseName,
        ISNULL(@ExecutedBy, SYSTEM_USER), @TrackITTicket, 'Running', 1
    );
    
    SET @ExecutionID = SCOPE_IDENTITY();
    
    -- Create step execution records
    INSERT INTO playbook.StepExecution (ExecutionID, StepID, StepNumber, Status)
    SELECT @ExecutionID, StepID, StepNumber, 'Pending'
    FROM playbook.Steps
    WHERE PlaybookID = @PlaybookID
    ORDER BY StepNumber;
    
    -- Update incident with playbook link
    IF @IncidentID IS NOT NULL
    BEGIN
        UPDATE correlation.Incidents
        SET PlaybookID = @PlaybookID
        WHERE IncidentID = @IncidentID;
    END
    
    SELECT @ExecutionID AS ExecutionID, 'Execution started' AS Status;
END

GO

-- [repl].[usp_CollectReplicationLatency]
CREATE OR ALTER PROCEDURE repl.usp_CollectReplicationLatency
AS
BEGIN
    SET NOCOUNT ON;
    IF EXISTS (SELECT 1 FROM sys.databases WHERE name = 'distribution' AND state = 0)
        PRINT 'Collecting replication latency from distribution database';
    ELSE
        PRINT 'Server is not a distributor - skipping';
END

GO

-- [replmon].[usp_CollectLatencyMetrics]

CREATE PROCEDURE replmon.usp_CollectLatencyMetrics
AS
BEGIN
    SET NOCOUNT ON;
    
    -- Collect latency from tracer tokens if available
    -- This is a placeholder - actual implementation depends on tracer token setup
    
    INSERT INTO replmon.LatencyHistory (PublisherServer, PublisherDB, PublicationName, SubscriberServer, LatencySeconds, MeasuredAt)
    SELECT 
        PublisherServer,
        PublisherDB,
        PublicationName,
        SubscriberServer,
        LatencySeconds,
        GETDATE()
    FROM replmon.SubscriptionStatus
    WHERE CollectedDate > DATEADD(MINUTE, -5, GETDATE());
END

GO

-- [replmon].[usp_CollectReplicationLatency]

    CREATE PROCEDURE replmon.usp_CollectReplicationLatency
    AS
    BEGIN
        SET NOCOUNT ON;
        
        -- Collect replication latency metrics
        SELECT 
            @@SERVERNAME AS ServerName,
            'Latency Collection' AS CheckType,
            SYSDATETIME() AS CheckTime;
        
        PRINT 'Replication latency collection completed';
    END;
    
GO

-- [replmon].[usp_MonitorReplication]

CREATE PROCEDURE replmon.usp_MonitorReplication
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @SQL NVARCHAR(MAX);
    DECLARE @LinkedServer NVARCHAR(256);
    
    DECLARE server_cursor CURSOR LOCAL FAST_FORWARD FOR
        SELECT ServerName FROM config.ServerInventory WHERE IsActive = 1;
    
    OPEN server_cursor;
    FETCH NEXT FROM server_cursor INTO @LinkedServer;
    
    WHILE @@FETCH_STATUS = 0
    BEGIN
        BEGIN TRY
            -- Collect publication status
            SET @SQL = N'
            IF EXISTS (SELECT 1 FROM [' + @LinkedServer + '].master.sys.databases WHERE name = ''distribution'')
            BEGIN
                INSERT INTO DBAOpsRepository.replmon.PublicationStatus 
                    (PublisherServer, PublisherDB, PublicationName, PublicationType, Status, CollectedDate)
                SELECT 
                    ''' + @LinkedServer + ''',
                    publisher_db,
                    publication,
                    CASE publication_type
                        WHEN 0 THEN ''Transactional''
                        WHEN 1 THEN ''Snapshot''
                        WHEN 2 THEN ''Merge''
                    END,
                    ''Active'',
                    GETDATE()
                FROM [' + @LinkedServer + '].distribution.dbo.MSpublications;
            END';
            
            EXEC sp_executesql @SQL;
            
            -- Collect subscription status
            SET @SQL = N'
            IF EXISTS (SELECT 1 FROM [' + @LinkedServer + '].master.sys.databases WHERE name = ''distribution'')
            BEGIN
                INSERT INTO DBAOpsRepository.replmon.SubscriptionStatus 
                    (PublisherServer, PublisherDB, PublicationName, SubscriberServer, SubscriberDB, 
                     Status, PendingCommands, CollectedDate)
                SELECT 
                    ''' + @LinkedServer + ''',
                    publisher_db,
                    publication,
                    subscriber,
                    subscriber_db,
                    CASE status
                        WHEN 0 THEN ''Inactive''
                        WHEN 1 THEN ''Subscribed''
                        WHEN 2 THEN ''Active''
                    END,
                    0,
                    GETDATE()
                FROM [' + @LinkedServer + '].distribution.dbo.MSsubscriptions;
            END';
            
            EXEC sp_executesql @SQL;
            
        END TRY
        BEGIN CATCH
            PRINT 'Error monitoring replication on ' + @LinkedServer + ': ' + ERROR_MESSAGE();
        END CATCH
        
        FETCH NEXT FROM server_cursor INTO @LinkedServer;
    END
    
    CLOSE server_cursor;
    DEALLOCATE server_cursor;
END

GO

-- [report].[usp_CMSStatusReport]

-- Update the procedure to use DBAOpsProfile
CREATE PROCEDURE report.usp_CMSStatusReport
    @SendEmail BIT = 1,
    @EmailRecipients NVARCHAR(500) = 'SQLServerAdminNotification@YOURORG.gov'
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @CMSServerCount INT = 0, @DBAOpsServerCount INT = 0, @MissingFromCMS INT = 0;
    DECLARE @MissingFromDBAOps INT = 0, @LinkedServerCount INT = 0, @ProdServers INT = 0, @NonProdServers INT = 0;
    DECLARE @SyncStatus NVARCHAR(20) = 'Unknown', @SyncPercent DECIMAL(5,2) = 0;
    DECLARE @HTMLBody NVARCHAR(MAX) = '', @Subject NVARCHAR(255) = '';
    DECLARE @StatusColor NVARCHAR(20) = '#6c757d', @StatusIcon NVARCHAR(10) = '?';
    
    -- Gather metrics
    SELECT @CMSServerCount = ISNULL(COUNT(*), 0) FROM msdb.dbo.sysmanagement_shared_registered_servers_internal;
    SELECT @DBAOpsServerCount = ISNULL(COUNT(*), 0) FROM config.ServerInventory WHERE IsActive = 1;
    SELECT @ProdServers = ISNULL(SUM(CASE WHEN Environment = 'PROD' THEN 1 ELSE 0 END), 0),
           @NonProdServers = ISNULL(SUM(CASE WHEN Environment != 'PROD' THEN 1 ELSE 0 END), 0)
    FROM config.ServerInventory WHERE IsActive = 1;
    
    SELECT @MissingFromCMS = ISNULL(COUNT(*), 0) FROM config.ServerInventory si
    WHERE si.IsActive = 1 AND NOT EXISTS (SELECT 1 FROM msdb.dbo.sysmanagement_shared_registered_servers_internal cms WHERE cms.server_name = si.ServerName OR cms.name = si.ServerName);
    
    SELECT @MissingFromDBAOps = ISNULL(COUNT(*), 0) FROM msdb.dbo.sysmanagement_shared_registered_servers_internal cms
    WHERE NOT EXISTS (SELECT 1 FROM config.ServerInventory si WHERE si.IsActive = 1 AND (si.ServerName = cms.server_name OR si.ServerName = cms.name));
    
    SELECT @LinkedServerCount = ISNULL(COUNT(*), 0) FROM sys.servers WHERE is_linked = 1;
    
    IF @DBAOpsServerCount > 0 SET @SyncPercent = CAST(100.0 * (@DBAOpsServerCount - @MissingFromCMS) / @DBAOpsServerCount AS DECIMAL(5,2));
    
    IF @MissingFromCMS = 0 AND @MissingFromDBAOps = 0
    BEGIN SET @SyncStatus = 'IN SYNC'; SET @StatusColor = '#28a745'; SET @StatusIcon = 'âœ“'; END
    ELSE IF @MissingFromCMS <= 5 AND @MissingFromDBAOps <= 5
    BEGIN SET @SyncStatus = 'MINOR DRIFT'; SET @StatusColor = '#ffc107'; SET @StatusIcon = 'âš '; END
    ELSE BEGIN SET @SyncStatus = 'OUT OF SYNC'; SET @StatusColor = '#dc3545'; SET @StatusIcon = 'âœ—'; END
    
    -- Save history
    INSERT INTO fact.CMSStatusHistory (CaptureDate, CMSServerCount, DBAOpsServerCount, MissingFromCMS, MissingFromDBAOps, LinkedServerCount, ProdServers, NonProdServers, SyncStatus, SyncPercent, CaptureTime)
    VALUES (CAST(SYSDATETIME() AS DATE), @CMSServerCount, @DBAOpsServerCount, @MissingFromCMS, @MissingFromDBAOps, @LinkedServerCount, @ProdServers, @NonProdServers, @SyncStatus, @SyncPercent, SYSDATETIME());
    
    SET @Subject = @StatusIcon + ' CMS Daily Status Report - ' + @SyncStatus + ' - ' + FORMAT(SYSDATETIME(), 'MMM dd, yyyy');
    
    SET @HTMLBody = '<!DOCTYPE html><html><head><style>
        body{font-family:Segoe UI,Arial,sans-serif;margin:0;padding:20px;background:#f5f5f5}
        .container{max-width:800px;margin:0 auto;background:white;border-radius:8px;box-shadow:0 2px 10px rgba(0,0,0,0.1)}
        .header{background:' + @StatusColor + ';color:white;padding:30px;text-align:center;border-radius:8px 8px 0 0}
        .header h1{margin:0;font-size:48px} .header h2{margin:10px 0 0 0;font-weight:normal}
        .metrics{display:flex;flex-wrap:wrap;padding:20px;background:#f8f9fa;justify-content:center}
        .metric{text-align:center;padding:15px 25px}
        .metric-value{font-size:32px;font-weight:bold;color:#333}
        .metric-label{font-size:12px;color:#666;text-transform:uppercase}
        .section{padding:20px 30px}
        .section-title{color:#333;border-bottom:2px solid #dee2e6;padding-bottom:10px;margin-bottom:15px}
        table{width:100%;border-collapse:collapse;margin:10px 0}
        th{background:#2c3e50;color:white;padding:12px;text-align:left}
        td{padding:10px 12px;border-bottom:1px solid #dee2e6}
        tr:hover{background:#f8f9fa}
        .badge{display:inline-block;padding:4px 12px;border-radius:20px;font-size:12px}
        .badge-success{background:#d4edda;color:#155724}
        .badge-warning{background:#fff3cd;color:#856404}
        .badge-danger{background:#f8d7da;color:#721c24}
        .badge-info{background:#d1ecf1;color:#0c5460}
        .footer{background:#f8f9fa;padding:15px 30px;text-align:center;color:#666;font-size:12px;border-top:1px solid #dee2e6;border-radius:0 0 8px 8px}
        .sync-bar{height:8px;background:#e9ecef;border-radius:4px;margin:10px 0}
        .sync-fill{height:100%;background:' + @StatusColor + ';border-radius:4px;width:' + CAST(@SyncPercent AS NVARCHAR(10)) + '%}
        .stat-row{display:flex;justify-content:space-between;padding:8px 0;border-bottom:1px solid #eee}
        </style></head><body><div class="container">
        <div class="header"><h1>' + CAST(@SyncPercent AS NVARCHAR(10)) + '%</h1><h2>CMS Sync Status: ' + @SyncStatus + '</h2></div>
        <div class="metrics">
            <div class="metric"><div class="metric-value">' + CAST(@CMSServerCount AS NVARCHAR(10)) + '</div><div class="metric-label">CMS Registered</div></div>
            <div class="metric"><div class="metric-value">' + CAST(@DBAOpsServerCount AS NVARCHAR(10)) + '</div><div class="metric-label">DBAOps Inventory</div></div>
            <div class="metric"><div class="metric-value">' + CAST(@ProdServers AS NVARCHAR(10)) + '</div><div class="metric-label">PROD Servers</div></div>
            <div class="metric"><div class="metric-value">' + CAST(@NonProdServers AS NVARCHAR(10)) + '</div><div class="metric-label">NON-PROD Servers</div></div>
            <div class="metric"><div class="metric-value">' + CAST(@LinkedServerCount AS NVARCHAR(10)) + '</div><div class="metric-label">Linked Servers</div></div>
        </div>
        <div class="section"><h3 class="section-title">ðŸ“Š Sync Overview</h3>
            <div class="sync-bar"><div class="sync-fill"></div></div>
            <div class="stat-row"><span>Sync Percentage</span><strong>' + CAST(@SyncPercent AS NVARCHAR(10)) + '%</strong></div>
            <div class="stat-row"><span>Missing from CMS</span><strong style="color:' + CASE WHEN @MissingFromCMS > 0 THEN '#dc3545' ELSE '#28a745' END + '">' + CAST(@MissingFromCMS AS NVARCHAR(10)) + '</strong></div>
            <div class="stat-row"><span>Missing from DBAOps</span><strong style="color:' + CASE WHEN @MissingFromDBAOps > 0 THEN '#dc3545' ELSE '#28a745' END + '">' + CAST(@MissingFromDBAOps AS NVARCHAR(10)) + '</strong></div>
        </div>';
    
    IF @MissingFromCMS > 0
    BEGIN
        SET @HTMLBody = @HTMLBody + '<div class="section"><h3 class="section-title">âš  Servers Missing from CMS (' + CAST(@MissingFromCMS AS NVARCHAR(10)) + ')</h3><table><tr><th>#</th><th>Server Name</th><th>Environment</th><th>SQL Version</th><th>Last Seen</th></tr>';
        DECLARE @MissingCMSRows NVARCHAR(MAX) = '';
        SELECT @MissingCMSRows = @MissingCMSRows + '<tr><td>' + CAST(ROW_NUMBER() OVER (ORDER BY si.Environment, si.ServerName) AS NVARCHAR(10)) + '</td><td><strong>' + ISNULL(si.ServerName, 'N/A') + '</strong></td><td><span class="badge ' + CASE WHEN si.Environment = 'PROD' THEN 'badge-danger' ELSE 'badge-info' END + '">' + ISNULL(si.Environment, 'Unknown') + '</span></td><td>' + ISNULL(LEFT(si.SQLVersion, 30), 'Unknown') + '</td><td>' + ISNULL(FORMAT(si.LastSeen, 'MMM dd HH:mm'), 'Never') + '</td></tr>'
        FROM config.ServerInventory si WHERE si.IsActive = 1 AND NOT EXISTS (SELECT 1 FROM msdb.dbo.sysmanagement_shared_registered_servers_internal cms WHERE cms.server_name = si.ServerName OR cms.name = si.ServerName) ORDER BY si.Environment, si.ServerName;
        SET @HTMLBody = @HTMLBody + @MissingCMSRows + '</table></div>';
    END
    
    IF @MissingFromDBAOps > 0
    BEGIN
        SET @HTMLBody = @HTMLBody + '<div class="section"><h3 class="section-title">âš  Servers in CMS Missing from DBAOps (' + CAST(@MissingFromDBAOps AS NVARCHAR(10)) + ')</h3><table><tr><th>#</th><th>Server Name</th><th>Connection String</th><th>CMS Group</th></tr>';
        DECLARE @MissingDBARows NVARCHAR(MAX) = '';
        SELECT @MissingDBARows = @MissingDBARows + '<tr><td>' + CAST(ROW_NUMBER() OVER (ORDER BY g.name, cms.name) AS NVARCHAR(10)) + '</td><td><strong>' + ISNULL(cms.name, 'N/A') + '</strong></td><td>' + ISNULL(cms.server_name, 'N/A') + '</td><td><span class="badge badge-info">' + ISNULL(g.name, 'Ungrouped') + '</span></td></tr>'
        FROM msdb.dbo.sysmanagement_shared_registered_servers_internal cms LEFT JOIN msdb.dbo.sysmanagement_shared_server_groups_internal g ON cms.server_group_id = g.server_group_id
        WHERE NOT EXISTS (SELECT 1 FROM config.ServerInventory si WHERE si.IsActive = 1 AND (si.ServerName = cms.server_name OR si.ServerName = cms.name)) ORDER BY g.name, cms.name;
        SET @HTMLBody = @HTMLBody + @MissingDBARows + '</table></div>';
    END
    
    SET @HTMLBody = @HTMLBody + '<div class="section"><h3 class="section-title">ðŸ“ CMS Server Groups</h3><table><tr><th>Group Name</th><th>Server Count</th><th>Status</th></tr>';
    DECLARE @GroupRows NVARCHAR(MAX) = '';
    SELECT @GroupRows = @GroupRows + '<tr><td><strong>' + ISNULL(g.name, 'Ungrouped') + '</strong></td><td>' + CAST(COUNT(s.server_id) AS NVARCHAR(10)) + '</td><td><span class="badge badge-success">Active</span></td></tr>'
    FROM msdb.dbo.sysmanagement_shared_registered_servers_internal s LEFT JOIN msdb.dbo.sysmanagement_shared_server_groups_internal g ON s.server_group_id = g.server_group_id GROUP BY g.name ORDER BY COUNT(s.server_id) DESC;
    IF @GroupRows = '' SET @GroupRows = '<tr><td colspan="3" style="text-align:center;color:#666">No CMS groups configured</td></tr>';
    SET @HTMLBody = @HTMLBody + @GroupRows + '</table></div>';
    SET @HTMLBody = @HTMLBody + '<div class="footer"><p><strong>DBAOps Enterprise™ Framework</strong> | ' + @@SERVERNAME + ' | ' + FORMAT(SYSDATETIME(), 'MMMM dd, yyyy hh:mm tt') + '</p><p>Central Management Server Status Report | John Bosco RHOYLLE INNOVATION EMPIRE™</p></div></div></body></html>';
    
    IF @SendEmail = 1
    BEGIN
        EXEC msdb.dbo.sp_send_dbmail @profile_name = 'DBAOpsProfile', @recipients = @EmailRecipients, @subject = @Subject, @body = @HTMLBody, @body_format = 'HTML';
        PRINT 'CMS Status Report email sent to: ' + @EmailRecipients;
    END
    
    SELECT @SyncStatus AS SyncStatus, @SyncPercent AS SyncPercent, @CMSServerCount AS CMSServers, @DBAOpsServerCount AS DBAOpsServers, @MissingFromCMS AS MissingFromCMS, @MissingFromDBAOps AS MissingFromDBAOps, @ProdServers AS ProdServers, @NonProdServers AS NonProdServers, @LinkedServerCount AS LinkedServers;
    PRINT 'CMS Status Report completed successfully';
END;

GO

-- [report].[usp_Collect_PatchServicingAssessment]

CREATE   PROCEDURE report.usp_Collect_PatchServicingAssessment
AS
BEGIN
    SET NOCOUNT ON;
    
    SELECT 
        cms.ServerName,
        COALESCE(cms.Environment, cms.CMSEnvironment, 'Unknown') AS Environment,
        cms.SQLVersion,
        cms.SqlBuild,
        cms.SqlCumulativeUpdate AS CurrentCU,
        br.LatestCU AS TargetCU,
        br.EndOfSupport,
        br.IsSupported,
        CASE 
            WHEN cms.SqlCumulativeUpdate = br.LatestCU THEN 'Current'
            WHEN cms.SqlCumulativeUpdate IS NULL THEN 'Unknown'
            WHEN br.IsSupported = 0 THEN 'End of Support'
            ELSE 'Needs Update'
        END AS PatchStatus,
        cms.ComplianceTier,
        cms.LastPatchDate
    FROM config.ServerInventory cms
    LEFT JOIN Patching.SQLServerBuildReference br 
        ON cms.SQLVersion LIKE CAST(br.SQLMajorVersion AS VARCHAR) + '.%'
    WHERE cms.IsActive = 1
    AND (cms.ServerType = 'SQL Server' OR cms.ServerType IS NULL)
    ORDER BY 
        CASE WHEN br.IsSupported = 0 THEN 1 ELSE 2 END,
        cms.Environment,
        cms.ServerName;
END

GO

-- [report].[usp_DailyInfrastructureReport]

CREATE   PROCEDURE report.usp_DailyInfrastructureReport
    @SendEmail BIT = 1,
    @EmailRecipients NVARCHAR(500) = 'SQLServerAdminNotification@YOURORG.gov',
    @Verbose BIT = 0
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @ReportDate DATE = CAST(GETDATE() AS DATE);
    DECLARE @GeneratedAt DATETIME2 = SYSDATETIME();
    DECLARE @OverallStatus NVARCHAR(20) = 'HEALTHY';
    DECLARE @HealthScore INT = 100;
    DECLARE @TotalServers INT = 0, @ProdServers INT = 0, @NonProdServers INT = 0;
    DECLARE @ReachableServers INT = 0, @UnreachableServers INT = 0, @TotalDatabases INT = 0;
    DECLARE @AvgCPU DECIMAL(5,2) = 0, @AvgMemory DECIMAL(5,2) = 0;
    DECLARE @FailedJobs INT = 0, @BackupNonCompliant INT = 0, @LowDiskDrives INT = 0, @ReplIssues INT = 0;
    DECLARE @CMSServers INT = 0, @CMSSyncPercent DECIMAL(5,2) = 100, @MissingFromCMS INT = 0, @LinkedServers INT = 0;
    DECLARE @HTMLBody NVARCHAR(MAX) = '';
    DECLARE @Subject NVARCHAR(255) = '';
    DECLARE @StatusColor NVARCHAR(20) = '#28a745';
    DECLARE @StatusIcon NVARCHAR(10) = 'OK';
    
    SELECT @TotalServers = COUNT(*), 
           @ProdServers = SUM(CASE WHEN Environment = 'PROD' THEN 1 ELSE 0 END),
           @NonProdServers = SUM(CASE WHEN Environment != 'PROD' THEN 1 ELSE 0 END)
    FROM config.ServerInventory WHERE IsActive = 1;
    
    SELECT @ReachableServers = ISNULL(SUM(CASE WHEN Reachable = 1 THEN 1 ELSE 0 END), 0),
           @UnreachableServers = ISNULL(SUM(CASE WHEN Reachable = 0 THEN 1 ELSE 0 END), 0)
    FROM config.ServerReachability WHERE CheckedOn >= DATEADD(HOUR, -24, SYSDATETIME());
    
    IF OBJECT_ID('dbo.fact_DatabaseInventory', 'U') IS NOT NULL
        SELECT @TotalDatabases = ISNULL(COUNT(*), 0) FROM dbo.fact_DatabaseInventory;
    
    IF OBJECT_ID('fact.ServerHealth', 'U') IS NOT NULL
        SELECT @AvgCPU = ISNULL(AVG(CPUPercent), 0), @AvgMemory = ISNULL(AVG(MemoryPercent), 0)
        FROM fact.ServerHealth WHERE TimeCaptured >= DATEADD(MINUTE, -15, SYSDATETIME());
    
    IF @AvgCPU = 0 AND OBJECT_ID('DBAOps.ServerHealthTelemetry', 'U') IS NOT NULL
        SELECT @AvgCPU = ISNULL(AVG(CPUPercent), 0), @AvgMemory = ISNULL(AVG(MemoryPercent), 0)
        FROM DBAOps.ServerHealthTelemetry WHERE CaptureTime >= DATEADD(MINUTE, -15, SYSDATETIME());
    
    SELECT @FailedJobs = COUNT(DISTINCT j.name) FROM msdb.dbo.sysjobs j
    INNER JOIN msdb.dbo.sysjobhistory h ON j.job_id = h.job_id
    WHERE h.step_id = 0 AND h.run_status = 0 AND j.enabled = 1 AND j.name NOT LIKE '%Universal Health%'
    AND msdb.dbo.agent_datetime(h.run_date, h.run_time) >= DATEADD(HOUR, -24, GETDATE());
    
    IF OBJECT_ID('fact.BackupCompliance', 'U') IS NOT NULL
        SELECT @BackupNonCompliant = ISNULL(COUNT(*), 0) FROM fact.BackupCompliance 
        WHERE IsCompliant = 0 AND LoadDate >= DATEADD(DAY, -1, SYSDATETIME());
    
    IF OBJECT_ID('DBAOps.DiskSpaceHistory', 'U') IS NOT NULL
        SELECT @LowDiskDrives = ISNULL(COUNT(*), 0) FROM DBAOps.DiskSpaceHistory 
        WHERE PercentFree < 10 AND CollectionTime >= DATEADD(HOUR, -4, SYSDATETIME());
    
    IF OBJECT_ID('ctl.ReplicationHealth', 'U') IS NOT NULL
        SELECT @ReplIssues = ISNULL(COUNT(*), 0) FROM ctl.ReplicationHealth 
        WHERE IsHealthy = 0 AND CollectionTime >= DATEADD(HOUR, -1, SYSDATETIME());
    
    SELECT @CMSServers = COUNT(*) FROM msdb.dbo.sysmanagement_shared_registered_servers_internal;
    
    SELECT @MissingFromCMS = COUNT(*) FROM config.ServerInventory si WHERE si.IsActive = 1
    AND NOT EXISTS (SELECT 1 FROM msdb.dbo.sysmanagement_shared_registered_servers_internal cms 
                    WHERE cms.server_name = si.ServerName OR cms.name = si.ServerName);
    
    IF @TotalServers > 0 
        SET @CMSSyncPercent = CAST(100.0 * (@TotalServers - @MissingFromCMS) / @TotalServers AS DECIMAL(5,2));
    
    SELECT @LinkedServers = COUNT(*) FROM sys.servers WHERE is_linked = 1;
    
    SET @HealthScore = 100;
    IF @UnreachableServers > 0 SET @HealthScore = @HealthScore - (@UnreachableServers * 2);
    IF @FailedJobs > 0 SET @HealthScore = @HealthScore - (@FailedJobs * 3);
    IF @BackupNonCompliant > 0 SET @HealthScore = @HealthScore - (@BackupNonCompliant * 2);
    IF @LowDiskDrives > 0 SET @HealthScore = @HealthScore - (@LowDiskDrives * 5);
    IF @ReplIssues > 0 SET @HealthScore = @HealthScore - (@ReplIssues * 3);
    IF @AvgCPU > 80 SET @HealthScore = @HealthScore - 10;
    IF @AvgMemory > 85 SET @HealthScore = @HealthScore - 10;
    IF @HealthScore < 0 SET @HealthScore = 0;
    
    IF @HealthScore >= 90 
    BEGIN SET @OverallStatus = 'HEALTHY'; SET @StatusColor = '#28a745'; SET @StatusIcon = 'OK'; END
    ELSE IF @HealthScore >= 70 
    BEGIN SET @OverallStatus = 'WARNING'; SET @StatusColor = '#ffc107'; SET @StatusIcon = 'WARN'; END
    ELSE 
    BEGIN SET @OverallStatus = 'CRITICAL'; SET @StatusColor = '#dc3545'; SET @StatusIcon = 'CRIT'; END
    
    SET @Subject = 'DBAOps Daily Report - ' + @OverallStatus + ' (' + CAST(@HealthScore AS VARCHAR) + '/100) - ' + FORMAT(@GeneratedAt, 'MMM dd, yyyy');
    
    SET @HTMLBody = '<!DOCTYPE html><html><head><style>';
    SET @HTMLBody = @HTMLBody + 'body{font-family:Segoe UI,Arial;margin:0;padding:20px;background:#f5f5f5}';
    SET @HTMLBody = @HTMLBody + '.container{max-width:900px;margin:0 auto;background:white;border-radius:8px}';
    SET @HTMLBody = @HTMLBody + '.header{background:' + @StatusColor + ';color:white;padding:30px;text-align:center;border-radius:8px 8px 0 0}';
    SET @HTMLBody = @HTMLBody + '.header h1{margin:0;font-size:48px}.header h2{margin:10px 0 0 0;font-weight:normal}';
    SET @HTMLBody = @HTMLBody + '.metrics{display:flex;flex-wrap:wrap;padding:20px;background:#f8f9fa;justify-content:center}';
    SET @HTMLBody = @HTMLBody + '.metric{text-align:center;padding:15px 20px}';
    SET @HTMLBody = @HTMLBody + '.metric-value{font-size:28px;font-weight:bold;color:#333}.metric-label{font-size:11px;color:#666}';
    SET @HTMLBody = @HTMLBody + '.section{padding:20px 30px}.section-title{border-bottom:2px solid #dee2e6;padding-bottom:10px;margin-bottom:15px}';
    SET @HTMLBody = @HTMLBody + 'table{width:100%;border-collapse:collapse}th{background:#2c3e50;color:white;padding:12px;text-align:left}';
    SET @HTMLBody = @HTMLBody + 'td{padding:10px 12px;border-bottom:1px solid #dee2e6}';
    SET @HTMLBody = @HTMLBody + '.ok{color:#28a745}.warn{color:#ffc107}.crit{color:#dc3545}';
    SET @HTMLBody = @HTMLBody + '.footer{background:#f8f9fa;padding:15px;text-align:center;color:#666;font-size:12px;border-radius:0 0 8px 8px}';
    SET @HTMLBody = @HTMLBody + '</style></head><body><div class="container">';
    
    SET @HTMLBody = @HTMLBody + '<div class="header"><h1>' + CAST(@HealthScore AS VARCHAR) + '/100</h1>';
    SET @HTMLBody = @HTMLBody + '<h2>Infrastructure Health: ' + @OverallStatus + '</h2>';
    SET @HTMLBody = @HTMLBody + '<h3>' + FORMAT(@GeneratedAt, 'dddd, MMMM dd, yyyy h:mm tt') + '</h3></div>';
    
    SET @HTMLBody = @HTMLBody + '<div class="metrics">';
    SET @HTMLBody = @HTMLBody + '<div class="metric"><div class="metric-value">' + CAST(@TotalServers AS VARCHAR) + '</div><div class="metric-label">SERVERS</div></div>';
    SET @HTMLBody = @HTMLBody + '<div class="metric"><div class="metric-value">' + CAST(@ProdServers AS VARCHAR) + '</div><div class="metric-label">PROD</div></div>';
    SET @HTMLBody = @HTMLBody + '<div class="metric"><div class="metric-value">' + CAST(@NonProdServers AS VARCHAR) + '</div><div class="metric-label">NON-PROD</div></div>';
    SET @HTMLBody = @HTMLBody + '<div class="metric"><div class="metric-value">' + FORMAT(@TotalDatabases, 'N0') + '</div><div class="metric-label">DATABASES</div></div>';
    SET @HTMLBody = @HTMLBody + '<div class="metric"><div class="metric-value">' + CAST(@AvgCPU AS VARCHAR) + '%</div><div class="metric-label">CPU</div></div>';
    SET @HTMLBody = @HTMLBody + '<div class="metric"><div class="metric-value">' + CAST(@AvgMemory AS VARCHAR) + '%</div><div class="metric-label">MEMORY</div></div>';
    SET @HTMLBody = @HTMLBody + '<div class="metric"><div class="metric-value">' + CAST(@LinkedServers AS VARCHAR) + '</div><div class="metric-label">LINKED</div></div>';
    SET @HTMLBody = @HTMLBody + '</div>';
    
    SET @HTMLBody = @HTMLBody + '<div class="section"><h3 class="section-title">Status Checks</h3><table>';
    SET @HTMLBody = @HTMLBody + '<tr><th>Check</th><th>Status</th><th>Details</th></tr>';
    SET @HTMLBody = @HTMLBody + '<tr><td>Server Reachability</td><td class="' + CASE WHEN @UnreachableServers = 0 THEN 'ok">OK' ELSE 'crit">CRITICAL' END + '</td><td>' + CASE WHEN @UnreachableServers = 0 THEN 'All ' + CAST(@ReachableServers AS VARCHAR) + ' servers reachable' ELSE CAST(@UnreachableServers AS VARCHAR) + ' unreachable' END + '</td></tr>';
    SET @HTMLBody = @HTMLBody + '<tr><td>Backup Compliance</td><td class="' + CASE WHEN @BackupNonCompliant = 0 THEN 'ok">OK' ELSE 'crit">CRITICAL' END + '</td><td>' + CASE WHEN @BackupNonCompliant = 0 THEN 'All compliant' ELSE CAST(@BackupNonCompliant AS VARCHAR) + ' non-compliant' END + '</td></tr>';
    SET @HTMLBody = @HTMLBody + '<tr><td>Job Failures</td><td class="' + CASE WHEN @FailedJobs = 0 THEN 'ok">OK' ELSE 'warn">WARNING' END + '</td><td>' + CASE WHEN @FailedJobs = 0 THEN 'No failures in 24h' ELSE CAST(@FailedJobs AS VARCHAR) + ' failed in 24h' END + '</td></tr>';
    SET @HTMLBody = @HTMLBody + '<tr><td>Disk Space</td><td class="' + CASE WHEN @LowDiskDrives = 0 THEN 'ok">OK' ELSE 'warn">WARNING' END + '</td><td>' + CASE WHEN @LowDiskDrives = 0 THEN 'All drives OK' ELSE CAST(@LowDiskDrives AS VARCHAR) + ' drives low' END + '</td></tr>';
    SET @HTMLBody = @HTMLBody + '<tr><td>Replication</td><td class="' + CASE WHEN @ReplIssues = 0 THEN 'ok">OK' ELSE 'warn">WARNING' END + '</td><td>' + CASE WHEN @ReplIssues = 0 THEN 'All healthy' ELSE CAST(@ReplIssues AS VARCHAR) + ' issues' END + '</td></tr>';
    SET @HTMLBody = @HTMLBody + '<tr><td>CMS Sync</td><td class="' + CASE WHEN @CMSSyncPercent >= 100 THEN 'ok">OK' ELSE 'warn">WARNING' END + '</td><td>' + CAST(@CMSSyncPercent AS VARCHAR) + '% synced (' + CAST(@CMSServers AS VARCHAR) + ' registered)</td></tr>';
    SET @HTMLBody = @HTMLBody + '</table></div>';
    
    SET @HTMLBody = @HTMLBody + '<div class="section"><h3 class="section-title">Server Summary</h3><table>';
    SET @HTMLBody = @HTMLBody + '<tr><th>Environment</th><th>Count</th></tr>';
    SET @HTMLBody = @HTMLBody + '<tr><td>Production (PROD)</td><td>' + CAST(@ProdServers AS VARCHAR) + '</td></tr>';
    SET @HTMLBody = @HTMLBody + '<tr><td>Non-Production</td><td>' + CAST(@NonProdServers AS VARCHAR) + '</td></tr>';
    SET @HTMLBody = @HTMLBody + '<tr><td><strong>Total</strong></td><td><strong>' + CAST(@TotalServers AS VARCHAR) + '</strong></td></tr>';
    SET @HTMLBody = @HTMLBody + '</table></div>';
    
    SET @HTMLBody = @HTMLBody + '<div class="footer"><strong>DBAOps Enterprise™ Framework</strong> | ' + @@SERVERNAME + ' | ' + FORMAT(@GeneratedAt, 'MMMM dd, yyyy hh:mm tt');
    SET @HTMLBody = @HTMLBody + '<br>Daily Infrastructure Report | YourState DPS | John Bosco RHOYLLE INNOVATION EMPIRE™</div>';
    SET @HTMLBody = @HTMLBody + '</div></body></html>';
    
    INSERT INTO report.DailyHealthReport (ReportDate, OverallStatus, HealthScore, TotalServers, ProdServers, NonProdServers,
        ReachableServers, UnreachableServers, TotalDatabases, AvgCPUPercent, AvgMemoryPercent, FailedJobs, BackupNonCompliant,
        LowDiskDrives, ReplicationIssues, CMSSyncPercent, ReportContent, GeneratedAt)
    VALUES (@ReportDate, @OverallStatus, @HealthScore, @TotalServers, @ProdServers, @NonProdServers, @ReachableServers,
        @UnreachableServers, @TotalDatabases, @AvgCPU, @AvgMemory, @FailedJobs, @BackupNonCompliant, @LowDiskDrives,
        @ReplIssues, @CMSSyncPercent, @HTMLBody, @GeneratedAt);
    
    IF @SendEmail = 1
    BEGIN
        EXEC msdb.dbo.sp_send_dbmail @profile_name = 'DBAOpsProfile', @recipients = @EmailRecipients,
            @subject = @Subject, @body = @HTMLBody, @body_format = 'HTML';
        PRINT 'Report sent to: ' + @EmailRecipients;
    END
    
    IF @Verbose = 1
        SELECT @OverallStatus AS Status, @HealthScore AS Score, @TotalServers AS Servers, @ProdServers AS PROD,
               @NonProdServers AS NonPROD, @ReachableServers AS Reachable, @UnreachableServers AS Unreachable,
               @FailedJobs AS FailedJobs, @AvgCPU AS CPU, @AvgMemory AS Memory, @CMSSyncPercent AS CMSSync;
    
    PRINT 'Daily Report Complete - ' + @OverallStatus + ' (' + CAST(@HealthScore AS VARCHAR) + '/100)';
END;

GO

-- [report].[usp_DailySecurityReport]

CREATE   PROCEDURE report.usp_DailySecurityReport
    @SendEmail BIT = 1,
    @EmailRecipients NVARCHAR(500) = 'SQLServerAdminNotification@YOURORG.gov',
    @Verbose BIT = 0
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @GeneratedAt DATETIME2 = SYSDATETIME();
    DECLARE @HTMLBody NVARCHAR(MAX) = '';
    DECLARE @Subject NVARCHAR(255) = '';
    DECLARE @StatusColor NVARCHAR(20) = '#28a745';
    DECLARE @SecurityScore INT = 100;
    DECLARE @OverallStatus NVARCHAR(20) = 'SECURE';
    
    -- Metrics
    DECLARE @LoginFailures24h INT = 0;
    DECLARE @PermissionChanges24h INT = 0;
    DECLARE @DDLChanges24h INT = 0;
    DECLARE @TotalLogins INT = 0;
    DECLARE @SysAdminCount INT = 0;
    DECLARE @DisabledLogins INT = 0;
    DECLARE @PermissionDriftCount INT = 0;
    DECLARE @MissingLocalAdmins INT = 0;
    DECLARE @ServiceAccounts INT = 0;
    DECLARE @TotalServerPermissions INT = 0;
    DECLARE @TotalDatabasePermissions INT = 0;
    
    -- Gather metrics
    IF OBJECT_ID('audit.LoginFailures', 'U') IS NOT NULL
        SELECT @LoginFailures24h = ISNULL(COUNT(*), 0) FROM audit.LoginFailures 
        WHERE EventTime >= DATEADD(HOUR, -24, SYSDATETIME());
    
    IF OBJECT_ID('audit.PermissionChanges', 'U') IS NOT NULL
        SELECT @PermissionChanges24h = ISNULL(COUNT(*), 0) FROM audit.PermissionChanges 
        WHERE ChangeTime >= DATEADD(HOUR, -24, SYSDATETIME());
    
    IF OBJECT_ID('audit.ObjectChanges', 'U') IS NOT NULL
        SELECT @DDLChanges24h = ISNULL(COUNT(*), 0) FROM audit.ObjectChanges 
        WHERE EventTime >= DATEADD(HOUR, -24, SYSDATETIME());
    
    IF OBJECT_ID('security.LoginInventory', 'U') IS NOT NULL
    BEGIN
        SELECT @TotalLogins = ISNULL(COUNT(*), 0),
               @SysAdminCount = ISNULL(SUM(CASE WHEN IsSysAdmin = 1 THEN 1 ELSE 0 END), 0),
               @DisabledLogins = ISNULL(SUM(CASE WHEN IsDisabled = 1 THEN 1 ELSE 0 END), 0)
        FROM security.LoginInventory;
    END
    
    IF OBJECT_ID('security.PermissionDrift', 'U') IS NOT NULL
        SELECT @PermissionDriftCount = ISNULL(COUNT(*), 0) FROM security.PermissionDrift;
    
    IF OBJECT_ID('security.MissingLocalAdminCompliance', 'U') IS NOT NULL
        SELECT @MissingLocalAdmins = ISNULL(COUNT(*), 0) FROM security.MissingLocalAdminCompliance;
    
    IF OBJECT_ID('security.ServiceAccounts', 'U') IS NOT NULL
        SELECT @ServiceAccounts = ISNULL(COUNT(*), 0) FROM security.ServiceAccounts;
    
    IF OBJECT_ID('security.ServerPermissions', 'U') IS NOT NULL
        SELECT @TotalServerPermissions = ISNULL(COUNT(*), 0) FROM security.ServerPermissions;
    
    IF OBJECT_ID('security.DatabasePermissions', 'U') IS NOT NULL
        SELECT @TotalDatabasePermissions = ISNULL(COUNT(*), 0) FROM security.DatabasePermissions;
    
    -- Calculate security score
    SET @SecurityScore = 100;
    IF @LoginFailures24h > 100 SET @SecurityScore = @SecurityScore - 15;
    ELSE IF @LoginFailures24h > 50 SET @SecurityScore = @SecurityScore - 10;
    ELSE IF @LoginFailures24h > 20 SET @SecurityScore = @SecurityScore - 5;
    
    IF @PermissionChanges24h > 10 SET @SecurityScore = @SecurityScore - 10;
    ELSE IF @PermissionChanges24h > 5 SET @SecurityScore = @SecurityScore - 5;
    
    IF @PermissionDriftCount > 100 SET @SecurityScore = @SecurityScore - 15;
    ELSE IF @PermissionDriftCount > 50 SET @SecurityScore = @SecurityScore - 10;
    
    IF @MissingLocalAdmins > 10 SET @SecurityScore = @SecurityScore - 10;
    ELSE IF @MissingLocalAdmins > 0 SET @SecurityScore = @SecurityScore - 5;
    
    IF @SysAdminCount > 20 SET @SecurityScore = @SecurityScore - 10;
    
    IF @SecurityScore < 0 SET @SecurityScore = 0;
    
    -- Determine status
    IF @SecurityScore >= 85 
    BEGIN SET @OverallStatus = 'SECURE'; SET @StatusColor = '#28a745'; END
    ELSE IF @SecurityScore >= 70 
    BEGIN SET @OverallStatus = 'ATTENTION'; SET @StatusColor = '#ffc107'; END
    ELSE 
    BEGIN SET @OverallStatus = 'AT RISK'; SET @StatusColor = '#dc3545'; END
    
    SET @Subject = 'DBAOps Security Report - ' + @OverallStatus + ' (' + CAST(@SecurityScore AS VARCHAR) + '/100) - ' + FORMAT(@GeneratedAt, 'MMM dd, yyyy');
    
    -- Build HTML
    SET @HTMLBody = '<!DOCTYPE html><html><head><style>';
    SET @HTMLBody = @HTMLBody + 'body{font-family:Segoe UI,Arial;margin:0;padding:20px;background:#f5f5f5}';
    SET @HTMLBody = @HTMLBody + '.container{max-width:900px;margin:0 auto;background:white;border-radius:8px}';
    SET @HTMLBody = @HTMLBody + '.header{background:' + @StatusColor + ';color:white;padding:30px;text-align:center;border-radius:8px 8px 0 0}';
    SET @HTMLBody = @HTMLBody + '.header h1{margin:0;font-size:48px}.header h2{margin:10px 0 0 0;font-weight:normal}';
    SET @HTMLBody = @HTMLBody + '.metrics{display:flex;flex-wrap:wrap;padding:20px;background:#f8f9fa;justify-content:center}';
    SET @HTMLBody = @HTMLBody + '.metric{text-align:center;padding:15px 20px}';
    SET @HTMLBody = @HTMLBody + '.metric-value{font-size:28px;font-weight:bold;color:#333}.metric-label{font-size:11px;color:#666}';
    SET @HTMLBody = @HTMLBody + '.section{padding:20px 30px}.section-title{border-bottom:2px solid #dee2e6;padding-bottom:10px;margin-bottom:15px}';
    SET @HTMLBody = @HTMLBody + 'table{width:100%;border-collapse:collapse}th{background:#2c3e50;color:white;padding:12px;text-align:left}';
    SET @HTMLBody = @HTMLBody + 'td{padding:10px 12px;border-bottom:1px solid #dee2e6}';
    SET @HTMLBody = @HTMLBody + '.ok{color:#28a745}.warn{color:#ffc107}.crit{color:#dc3545}';
    SET @HTMLBody = @HTMLBody + '.footer{background:#f8f9fa;padding:15px;text-align:center;color:#666;font-size:12px;border-radius:0 0 8px 8px}';
    SET @HTMLBody = @HTMLBody + '</style></head><body><div class="container">';
    
    -- Header
    SET @HTMLBody = @HTMLBody + '<div class="header"><h1>' + CAST(@SecurityScore AS VARCHAR) + '/100</h1>';
    SET @HTMLBody = @HTMLBody + '<h2>Security Status: ' + @OverallStatus + '</h2>';
    SET @HTMLBody = @HTMLBody + '<h3>' + FORMAT(@GeneratedAt, 'dddd, MMMM dd, yyyy h:mm tt') + '</h3></div>';
    
    -- Metrics bar
    SET @HTMLBody = @HTMLBody + '<div class="metrics">';
    SET @HTMLBody = @HTMLBody + '<div class="metric"><div class="metric-value">' + CAST(@LoginFailures24h AS VARCHAR) + '</div><div class="metric-label">LOGIN FAILURES (24h)</div></div>';
    SET @HTMLBody = @HTMLBody + '<div class="metric"><div class="metric-value">' + CAST(@PermissionChanges24h AS VARCHAR) + '</div><div class="metric-label">PERMISSION CHANGES</div></div>';
    SET @HTMLBody = @HTMLBody + '<div class="metric"><div class="metric-value">' + CAST(@DDLChanges24h AS VARCHAR) + '</div><div class="metric-label">DDL CHANGES</div></div>';
    SET @HTMLBody = @HTMLBody + '<div class="metric"><div class="metric-value">' + CAST(@TotalLogins AS VARCHAR) + '</div><div class="metric-label">TOTAL LOGINS</div></div>';
    SET @HTMLBody = @HTMLBody + '<div class="metric"><div class="metric-value">' + CAST(@SysAdminCount AS VARCHAR) + '</div><div class="metric-label">SYSADMINS</div></div>';
    SET @HTMLBody = @HTMLBody + '<div class="metric"><div class="metric-value">' + CAST(@PermissionDriftCount AS VARCHAR) + '</div><div class="metric-label">PERMISSION DRIFT</div></div>';
    SET @HTMLBody = @HTMLBody + '</div>';
    
    -- Security Checks
    SET @HTMLBody = @HTMLBody + '<div class="section"><h3 class="section-title">Security Checks</h3><table>';
    SET @HTMLBody = @HTMLBody + '<tr><th>Check</th><th>Status</th><th>Details</th></tr>';
    SET @HTMLBody = @HTMLBody + '<tr><td>Login Failures (24h)</td><td class="' + CASE WHEN @LoginFailures24h < 20 THEN 'ok">OK' WHEN @LoginFailures24h < 50 THEN 'warn">ATTENTION' ELSE 'crit">HIGH' END + '</td><td>' + CAST(@LoginFailures24h AS VARCHAR) + ' failed attempts</td></tr>';
    SET @HTMLBody = @HTMLBody + '<tr><td>Permission Changes (24h)</td><td class="' + CASE WHEN @PermissionChanges24h = 0 THEN 'ok">OK' WHEN @PermissionChanges24h < 5 THEN 'warn">ATTENTION' ELSE 'crit">HIGH' END + '</td><td>' + CAST(@PermissionChanges24h AS VARCHAR) + ' changes detected</td></tr>';
    SET @HTMLBody = @HTMLBody + '<tr><td>Permission Drift</td><td class="' + CASE WHEN @PermissionDriftCount < 50 THEN 'ok">OK' WHEN @PermissionDriftCount < 100 THEN 'warn">ATTENTION' ELSE 'crit">HIGH' END + '</td><td>' + CAST(@PermissionDriftCount AS VARCHAR) + ' drift records</td></tr>';
    SET @HTMLBody = @HTMLBody + '<tr><td>SysAdmin Count</td><td class="' + CASE WHEN @SysAdminCount <= 10 THEN 'ok">OK' WHEN @SysAdminCount <= 20 THEN 'warn">ATTENTION' ELSE 'crit">HIGH' END + '</td><td>' + CAST(@SysAdminCount AS VARCHAR) + ' sysadmin logins</td></tr>';
    SET @HTMLBody = @HTMLBody + '<tr><td>Missing Local Admins</td><td class="' + CASE WHEN @MissingLocalAdmins = 0 THEN 'ok">OK' ELSE 'warn">ATTENTION' END + '</td><td>' + CAST(@MissingLocalAdmins AS VARCHAR) + ' servers non-compliant</td></tr>';
    SET @HTMLBody = @HTMLBody + '<tr><td>Disabled Logins</td><td class="ok">INFO</td><td>' + CAST(@DisabledLogins AS VARCHAR) + ' disabled logins</td></tr>';
    SET @HTMLBody = @HTMLBody + '</table></div>';
    
    -- Permission Summary
    SET @HTMLBody = @HTMLBody + '<div class="section"><h3 class="section-title">Permission Summary</h3><table>';
    SET @HTMLBody = @HTMLBody + '<tr><th>Category</th><th>Count</th></tr>';
    SET @HTMLBody = @HTMLBody + '<tr><td>Server Permissions</td><td>' + FORMAT(@TotalServerPermissions, 'N0') + '</td></tr>';
    SET @HTMLBody = @HTMLBody + '<tr><td>Database Permissions</td><td>' + FORMAT(@TotalDatabasePermissions, 'N0') + '</td></tr>';
    SET @HTMLBody = @HTMLBody + '<tr><td>Service Accounts</td><td>' + CAST(@ServiceAccounts AS VARCHAR) + '</td></tr>';
    SET @HTMLBody = @HTMLBody + '<tr><td>Total Logins Inventoried</td><td>' + CAST(@TotalLogins AS VARCHAR) + '</td></tr>';
    SET @HTMLBody = @HTMLBody + '</table></div>';
    
    -- Recent Login Failures (Top 10)
    IF @LoginFailures24h > 0
    BEGIN
        SET @HTMLBody = @HTMLBody + '<div class="section"><h3 class="section-title">Recent Login Failures (Top 10)</h3><table>';
        SET @HTMLBody = @HTMLBody + '<tr><th>Login Name</th><th>Server</th><th>Time</th><th>Client</th></tr>';
        
        DECLARE @FailureRows NVARCHAR(MAX) = '';
        SELECT TOP 10 @FailureRows = @FailureRows + '<tr><td>' + ISNULL(LoginName, 'Unknown') + '</td><td>' + ISNULL(ServerName, 'N/A') + '</td><td>' + ISNULL(FORMAT(EventTime, 'MMM dd HH:mm'), 'N/A') + '</td><td>' + ISNULL(LEFT(ClientHost, 20), 'N/A') + '</td></tr>'
        FROM audit.LoginFailures
        WHERE EventTime >= DATEADD(HOUR, -24, SYSDATETIME())
        ORDER BY EventTime DESC;
        
        SET @HTMLBody = @HTMLBody + @FailureRows + '</table></div>';
    END
    
    -- Footer
    SET @HTMLBody = @HTMLBody + '<div class="footer"><strong>DBAOps Security Framework</strong> | ' + @@SERVERNAME + ' | ' + FORMAT(@GeneratedAt, 'MMMM dd, yyyy hh:mm tt');
    SET @HTMLBody = @HTMLBody + '<br>Daily Security Report | YourState DPS | John Bosco RHOYLLE INNOVATION EMPIRE™</div>';
    SET @HTMLBody = @HTMLBody + '</div></body></html>';
    
    -- Send email
    IF @SendEmail = 1
    BEGIN
        EXEC msdb.dbo.sp_send_dbmail @profile_name = 'DBAOpsProfile', @recipients = @EmailRecipients,
            @subject = @Subject, @body = @HTMLBody, @body_format = 'HTML';
        PRINT 'Security Report sent to: ' + @EmailRecipients;
    END
    
    IF @Verbose = 1
        SELECT @OverallStatus AS Status, @SecurityScore AS Score, @LoginFailures24h AS LoginFailures,
               @PermissionChanges24h AS PermChanges, @DDLChanges24h AS DDLChanges, @TotalLogins AS Logins,
               @SysAdminCount AS SysAdmins, @PermissionDriftCount AS PermDrift, @MissingLocalAdmins AS MissingAdmins;
    
    PRINT 'Security Report Complete - ' + @OverallStatus + ' (' + CAST(@SecurityScore AS VARCHAR) + '/100)';
END;

GO

-- [report].[usp_Email_PatchServicingAssessment]

CREATE PROCEDURE report.usp_Email_PatchServicingAssessment
(
    @ProfileName sysname,
    @Recipients  nvarchar(max),
    @Subject     nvarchar(300) = N'DBAOps - SQL Servicing Currency & Microsoft Support Report',
    @OnlyOnIssues BIT = 1  -- NEW: Only send if issues exist
)
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @RunId bigint = (SELECT MAX(RunId) FROM report.PatchServicingSummary);
    IF @RunId IS NULL
    BEGIN
        RAISERROR('No report runs found in report.PatchServicingSummary.', 16, 1);
        RETURN;
    END;
    
    DECLARE @Behind int, @Out int;
    
    SELECT
        @Behind = BehindServers,
        @Out = OutOfSupportServers
    FROM report.PatchServicingSummary
    WHERE RunId = @RunId;
    
    -- =====================================================
    -- SMART EMAIL LOGIC: Only send if issues exist
    -- =====================================================
    IF @OnlyOnIssues = 1 AND ISNULL(@Behind, 0) = 0 AND ISNULL(@Out, 0) = 0
    BEGIN
        PRINT 'DBAOps | JB: All servers current and supported. Email not sent.';
        RETURN;
    END;
    
    DECLARE @Body nvarchar(max) = N'';
    SET @Body = N'<html><body style="font-family:Segoe UI,Arial;">
    <h2>DBAOps - SQL Servicing Currency Report</h2>
    <p><strong>This report only sent when issues exist.</strong></p>
    <table border="1" cellpadding="8">
    <tr><td>Out of Support</td><td style="color:red;font-weight:bold;">' + CAST(ISNULL(@Out, 0) AS VARCHAR) + N'</td></tr>
    <tr><td>Behind Latest CU</td><td style="color:orange;font-weight:bold;">' + CAST(ISNULL(@Behind, 0) AS VARCHAR) + N'</td></tr>
    </table>
    <p style="color:gray;">DBAOps Enterprise™ Framework | John Bosco RHOYLLE INNOVATION EMPIRE™</p>
    </body></html>';
    
    EXEC msdb.dbo.sp_send_dbmail
        @profile_name = @ProfileName,
        @recipients = @Recipients,
        @subject = @Subject,
        @body = @Body,
        @body_format = 'HTML';
    
    PRINT 'DBAOps | JB: Patch servicing email sent - ' + 
        CAST(ISNULL(@Out, 0) AS VARCHAR) + ' out of support, ' +
        CAST(ISNULL(@Behind, 0) AS VARCHAR) + ' behind';
END;

GO

-- [report].[usp_Email_SqlServicingCurrency_Report]

CREATE PROCEDURE report.usp_Email_SqlServicingCurrency_Report
(
    @DistributionName sysname = N'DBAOps_SqlServicingWeekly',
    @SendEmail        bit = 1,
    @NearEosDays      int = 180
)
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @ToList nvarchar(max);
    DECLARE @CcList nvarchar(max);
    DECLARE @ProfileName sysname;
    DECLARE @Subject nvarchar(400);
    DECLARE @BodyHtml nvarchar(max);
    DECLARE @RunStamp nvarchar(30);
    
    SET @RunStamp = CONVERT(nvarchar(30), SYSDATETIME(), 120);

    SELECT @ToList = ToList, @CcList = CcList, @ProfileName = MailProfileName
    FROM config.EmailDistribution
    WHERE DistributionName = @DistributionName AND IsEnabled = 1;

    IF @ToList IS NULL
    BEGIN
        RAISERROR('DBAOps | JB: Email distribution not found or disabled: %s', 16, 1, @DistributionName);
        RETURN;
    END;

    IF @ProfileName IS NULL
        SELECT TOP (1) @ProfileName = name FROM msdb.dbo.sysmail_profile ORDER BY name;

    SET @Subject = N'DBAOps | JB | SQL Servicing Currency and Support Lifecycle Report | ' + @RunStamp;

    -- Create temp table to hold assessment results
    CREATE TABLE #AssessmentResults
    (
        InventoryServerRaw nvarchar(256),
        InventoryServerKey nvarchar(256),
        StatusJoinKey nvarchar(256),
        ServerName nvarchar(256),
        ServerEnvironment nvarchar(50),
        ComplianceStatus nvarchar(50),
        SQLVersion nvarchar(100),
        CurrentCULevel nvarchar(50),
        CurrentCUNumber int,
        TargetCUNumber int,
        TargetBuild nvarchar(50),
        TargetKB nvarchar(20),
        TargetRelease date,
        PatchCurrencyStatus nvarchar(30),
        ExtendedSupportEnd date,
        SupportStatus nvarchar(30),
        UpgradeRecommendation nvarchar(50),
        MappingApplied bit
    );

    INSERT #AssessmentResults
    EXEC report.usp_SqlServicingCurrency_Assessment @Mode = 'DETAIL', @NearEosDays = @NearEosDays;

    -- Get counts
    DECLARE @Total int, @Compliant int, @NonCompliant int, @Unknown int, @NearEOS int, @OutOfSupport int;
    
    SELECT 
        @Total = COUNT(1),
        @Compliant = SUM(CASE WHEN ComplianceStatus = N'COMPLIANT' THEN 1 ELSE 0 END),
        @NonCompliant = SUM(CASE WHEN ComplianceStatus = N'NON-COMPLIANT' THEN 1 ELSE 0 END),
        @Unknown = SUM(CASE WHEN ComplianceStatus = N'UNKNOWN' THEN 1 ELSE 0 END),
        @NearEOS = SUM(CASE WHEN SupportStatus = N'NEAR_EOS' THEN 1 ELSE 0 END),
        @OutOfSupport = SUM(CASE WHEN SupportStatus = N'OUT_OF_SUPPORT' THEN 1 ELSE 0 END)
    FROM #AssessmentResults;

    -- Only send if there are ANY servers that are NOT COMPLIANT
    IF NOT EXISTS (SELECT 1 FROM #AssessmentResults WHERE ComplianceStatus <> N'COMPLIANT')
    BEGIN
        DROP TABLE #AssessmentResults;
        PRINT 'DBAOps | JB: All servers are compliant. Email not sent.';
        RETURN;
    END;

    -- Build server lists
    DECLARE @NonCompliantList nvarchar(max) = N'';
    DECLARE @UnknownList nvarchar(max) = N'';
    DECLARE @NearEOSList nvarchar(max) = N'';
    DECLARE @OutOfSupportList nvarchar(max) = N'';

    -- Non-Compliant servers
    SELECT @NonCompliantList = @NonCompliantList + 
        N'<tr><td style="padding:2px 8px;">' + ISNULL(ServerName, InventoryServerKey) + 
        N'</td><td style="padding:2px 8px;">' + ISNULL(ServerEnvironment, N'N/A') + 
        N'</td><td style="padding:2px 8px;">' + ISNULL(SQLVersion, N'N/A') + 
        N'</td><td style="padding:2px 8px;">' + ISNULL(CurrentCULevel, N'N/A') + 
        N'</td><td style="padding:2px 8px;">' + ISNULL(CASE WHEN TargetCUNumber IS NOT NULL THEN N'CU' + CAST(TargetCUNumber AS nvarchar(10)) ELSE N'N/A' END, N'N/A') + 
        N'</td><td style="padding:2px 8px;">' + ISNULL(PatchCurrencyStatus, N'N/A') + 
        N'</td></tr>'
    FROM #AssessmentResults
    WHERE ComplianceStatus = N'NON-COMPLIANT'
    ORDER BY ServerEnvironment, ServerName;

    -- Unknown servers
    SELECT @UnknownList = @UnknownList + 
        N'<tr><td style="padding:2px 8px;">' + ISNULL(ServerName, InventoryServerKey) + 
        N'</td><td style="padding:2px 8px;">' + ISNULL(ServerEnvironment, N'N/A') + 
        N'</td><td style="padding:2px 8px;">' + ISNULL(SQLVersion, N'N/A') + 
        N'</td><td style="padding:2px 8px;">' + ISNULL(CurrentCULevel, N'N/A') + 
        N'</td><td style="padding:2px 8px;">' + ISNULL(PatchCurrencyStatus, N'N/A') + 
        N'</td></tr>'
    FROM #AssessmentResults
    WHERE ComplianceStatus = N'UNKNOWN'
    ORDER BY ServerEnvironment, ServerName;

    -- Near EOS servers (regardless of compliance)
    SELECT @NearEOSList = @NearEOSList + 
        N'<tr><td style="padding:2px 8px;">' + ISNULL(ServerName, InventoryServerKey) + 
        N'</td><td style="padding:2px 8px;">' + ISNULL(ServerEnvironment, N'N/A') + 
        N'</td><td style="padding:2px 8px;">' + ISNULL(SQLVersion, N'N/A') + 
        N'</td><td style="padding:2px 8px;">' + ISNULL(CONVERT(nvarchar(10), ExtendedSupportEnd, 120), N'N/A') + 
        N'</td></tr>'
    FROM #AssessmentResults
    WHERE SupportStatus = N'NEAR_EOS'
    ORDER BY ExtendedSupportEnd, ServerName;

    -- Out of Support servers
    SELECT @OutOfSupportList = @OutOfSupportList + 
        N'<tr><td style="padding:2px 8px;">' + ISNULL(ServerName, InventoryServerKey) + 
        N'</td><td style="padding:2px 8px;">' + ISNULL(ServerEnvironment, N'N/A') + 
        N'</td><td style="padding:2px 8px;">' + ISNULL(SQLVersion, N'N/A') + 
        N'</td><td style="padding:2px 8px;">' + ISNULL(CONVERT(nvarchar(10), ExtendedSupportEnd, 120), N'N/A') + 
        N'</td></tr>'
    FROM #AssessmentResults
    WHERE SupportStatus = N'OUT_OF_SUPPORT'
    ORDER BY ServerName;

    -- Build HTML body
    SET @BodyHtml = N'<html><head><style>
        body { font-family: Segoe UI, Arial, sans-serif; font-size: 12px; }
        h2 { color: #2E86AB; margin-top: 20px; margin-bottom: 10px; border-bottom: 2px solid #2E86AB; padding-bottom: 5px; }
        h3 { color: #A23B72; margin-top: 15px; margin-bottom: 8px; }
        table { border-collapse: collapse; margin: 10px 0; }
        th { background-color: #2E86AB; color: white; padding: 6px 10px; text-align: left; }
        td { padding: 4px 10px; border-bottom: 1px solid #ddd; }
        tr:nth-child(even) { background-color: #f9f9f9; }
        .summary-table td { padding: 4px 15px; }
        .summary-label { font-weight: bold; }
        .count-good { color: green; font-weight: bold; }
        .count-warning { color: orange; font-weight: bold; }
        .count-critical { color: red; font-weight: bold; }
        .section { margin-bottom: 25px; }
    </style></head><body>';

    -- Header
    SET @BodyHtml = @BodyHtml + 
        N'<h2>DBAOps | JB | SQL Servicing Currency and Support Lifecycle Report</h2>' +
        N'<p><b>Generated:</b> ' + @RunStamp + N'<br/>' +
        N'<b>Author:</b> RHOYLLE INNOVATION EMPIRE™, Sr SQL DBA - DBAOps Engineer<br/>' +
        N'<b>Trademark:</b> DBAOps | JB</p>';

    -- Summary Section
    SET @BodyHtml = @BodyHtml + 
        N'<div class="section"><h3>Compliance Summary</h3>' +
        N'<table class="summary-table">' +
        N'<tr><td class="summary-label">Total Servers:</td><td>' + CAST(@Total AS nvarchar(10)) + N'</td></tr>' +
        N'<tr><td class="summary-label">Compliant:</td><td class="count-good">' + CAST(@Compliant AS nvarchar(10)) + N'</td></tr>' +
        N'<tr><td class="summary-label">Non-Compliant:</td><td class="count-critical">' + CAST(@NonCompliant AS nvarchar(10)) + N'</td></tr>' +
        N'<tr><td class="summary-label">Unknown:</td><td class="count-warning">' + CAST(@Unknown AS nvarchar(10)) + N'</td></tr>' +
        N'<tr><td class="summary-label">Near End of Support:</td><td class="count-warning">' + CAST(@NearEOS AS nvarchar(10)) + N'</td></tr>' +
        N'<tr><td class="summary-label">Out of Support:</td><td class="count-critical">' + CAST(@OutOfSupport AS nvarchar(10)) + N'</td></tr>' +
        N'</table></div>';

    -- Out of Support Section (CRITICAL - show first)
    IF @OutOfSupport > 0
    BEGIN
        SET @BodyHtml = @BodyHtml + 
            N'<div class="section"><h3 style="color:red;">Out of Support Servers (' + CAST(@OutOfSupport AS nvarchar(10)) + N') - CRITICAL</h3>' +
            N'<table><tr><th>Server Name</th><th>Environment</th><th>SQL Version</th><th>Support Ended</th></tr>' +
            @OutOfSupportList +
            N'</table></div>';
    END;

    -- Non-Compliant Section
    IF @NonCompliant > 0
    BEGIN
        SET @BodyHtml = @BodyHtml + 
            N'<div class="section"><h3 style="color:red;">Non-Compliant Servers (' + CAST(@NonCompliant AS nvarchar(10)) + N')</h3>' +
            N'<table><tr><th>Server Name</th><th>Environment</th><th>SQL Version</th><th>Current CU</th><th>Target CU</th><th>Status</th></tr>' +
            @NonCompliantList +
            N'</table></div>';
    END;

    -- Unknown Section
    IF @Unknown > 0
    BEGIN
        SET @BodyHtml = @BodyHtml + 
            N'<div class="section"><h3 style="color:orange;">Unknown Status Servers (' + CAST(@Unknown AS nvarchar(10)) + N')</h3>' +
            N'<table><tr><th>Server Name</th><th>Environment</th><th>SQL Version</th><th>Current CU</th><th>Status</th></tr>' +
            @UnknownList +
            N'</table></div>';
    END;

    -- Near EOS Section
    IF @NearEOS > 0
    BEGIN
        SET @BodyHtml = @BodyHtml + 
            N'<div class="section"><h3 style="color:orange;">Near End of Support (' + CAST(@NearEOS AS nvarchar(10)) + N')</h3>' +
            N'<table><tr><th>Server Name</th><th>Environment</th><th>SQL Version</th><th>Support Ends</th></tr>' +
            @NearEOSList +
            N'</table></div>';
    END;

    -- Footer
    SET @BodyHtml = @BodyHtml + 
        N'<hr/><p><i>This report only includes non-compliant servers requiring attention.</i></p>' +
        N'</body></html>';

    DROP TABLE #AssessmentResults;

    IF @SendEmail = 0
    BEGIN
        SELECT 
            DBAOps_Subject = @Subject, 
            DBAOps_To = @ToList, 
            DBAOps_Profile = @ProfileName, 
            DBAOps_BodyHtml = @BodyHtml;
        RETURN;
    END;

    BEGIN TRY
        EXEC msdb.dbo.sp_send_dbmail
            @profile_name = @ProfileName,
            @recipients   = @ToList,
            @copy_recipients = @CcList,
            @subject      = @Subject,
            @body         = @BodyHtml,
            @body_format  = 'HTML';

        INSERT report.SqlServicingEmailLog (SubjectLine, ToList, Status, ErrorMessage)
        VALUES (@Subject, @ToList, N'SENT', NULL);
    END TRY
    BEGIN CATCH
        INSERT report.SqlServicingEmailLog (SubjectLine, ToList, Status, ErrorMessage)
        VALUES (@Subject, @ToList, N'FAILED', ERROR_MESSAGE());
        THROW;
    END CATCH;
END;

GO

-- [report].[usp_SqlServicingCurrency_Assessment]

CREATE PROCEDURE report.usp_SqlServicingCurrency_Assessment
(
    @Mode VARCHAR(20) = 'SUMMARY',
    @NearEosDays INT = 180
)
AS
BEGIN
    SET NOCOUNT ON;
    
    -- Use CMS as source of truth
    IF @Mode = 'DETAIL'
    BEGIN
        ;WITH CMS_Servers AS (
            SELECT DISTINCT server_name
            FROM msdb.dbo.sysmanagement_shared_registered_servers_internal
        )
        SELECT DISTINCT
            cms.server_name AS InventoryServerRaw,
            UPPER(cms.server_name) AS InventoryServerKey,
            UPPER(cms.server_name) AS StatusJoinKey,
            cms.server_name AS ServerName,
            ISNULL(p.Environment, 'UNKNOWN') AS ServerEnvironment,
            CASE 
                WHEN p.PatchStatus = 'Up to Date' THEN 'COMPLIANT'
                WHEN p.PatchStatus IS NULL THEN 'UNKNOWN'
                ELSE 'NON-COMPLIANT'
            END AS ComplianceStatus,
            CASE 
                WHEN p.SQLVersion IS NULL OR p.SQLVersion = 'Unknown' THEN
                    CASE 
                        WHEN p.CurrentBuild LIKE '17.%' THEN 'SQL Server 2025'
                        WHEN p.CurrentBuild LIKE '16.%' THEN 'SQL Server 2022'
                        WHEN p.CurrentBuild LIKE '15.%' THEN 'SQL Server 2019'
                        WHEN p.CurrentBuild LIKE '14.%' THEN 'SQL Server 2017'
                        WHEN p.CurrentBuild LIKE '13.%' THEN 'SQL Server 2016'
                        WHEN p.CurrentBuild LIKE '12.%' THEN 'SQL Server 2014'
                        ELSE ISNULL(p.SQLVersion, 'Unknown')
                    END
                WHEN p.SQLVersion = 'Other' THEN
                    CASE 
                        WHEN p.CurrentBuild LIKE '17.%' THEN 'SQL Server 2025'
                        WHEN p.CurrentBuild LIKE '16.%' THEN 'SQL Server 2022'
                        WHEN p.CurrentBuild LIKE '15.%' THEN 'SQL Server 2019'
                        WHEN p.CurrentBuild LIKE '14.%' THEN 'SQL Server 2017'
                        WHEN p.CurrentBuild LIKE '13.%' THEN 'SQL Server 2016'
                        WHEN p.CurrentBuild LIKE '12.%' THEN 'SQL Server 2014'
                        ELSE 'SQL Server (Version TBD)'
                    END
                ELSE p.SQLVersion
            END AS SQLVersion,
            ISNULL(p.CurrentCU, 'N/A') AS CurrentCULevel,
            TRY_CAST(REPLACE(REPLACE(REPLACE(p.CurrentCU, 'CU', ''), '+GDR', ''), 'SP3', '') AS INT) AS CurrentCUNumber,
            TRY_CAST(REPLACE(REPLACE(REPLACE(p.TargetCU, 'CU', ''), '+GDR', ''), 'SP3', '') AS INT) AS TargetCUNumber,
            ISNULL(p.CurrentBuild, 'N/A') AS TargetBuild,
            'N/A' AS TargetKB,
            CAST(NULL AS DATE) AS TargetRelease,
            CASE 
                WHEN p.PatchStatus = 'Up to Date' THEN 'UP_TO_DATE'
                WHEN p.PatchStatus = 'Needs Update' THEN 'NEEDS_PATCH'
                WHEN p.PatchStatus IS NULL THEN 'UNKNOWN_VERSION'
                ELSE 'REVIEW'
            END AS PatchCurrencyStatus,
            p.SupportEnds AS ExtendedSupportEnd,
            CASE 
                WHEN p.SupportStatus = 'End of Support' THEN 'OUT_OF_SUPPORT'
                WHEN p.DaysUntilEOS <= @NearEosDays THEN 'NEAR_EOS'
                WHEN p.SupportStatus = 'Supported' THEN 'SUPPORTED'
                ELSE 'UNKNOWN'
            END AS SupportStatus,
            CASE 
                WHEN p.DaysUntilEOS < 0 THEN 'UPGRADE_CRITICAL'
                WHEN p.DaysUntilEOS <= @NearEosDays THEN 'PLAN_UPGRADE'
                ELSE 'MAINTAIN_CURRENT'
            END AS UpgradeRecommendation,
            CASE WHEN p.ServerName IS NOT NULL THEN 1 ELSE 0 END AS MappingApplied
        FROM CMS_Servers cms
        LEFT JOIN Patching.vw_PatchComplianceDetail p 
            ON UPPER(cms.server_name) = UPPER(p.ServerName)
        ORDER BY cms.server_name;
    END
    ELSE
    BEGIN
        -- Summary mode
        ;WITH CMS_Servers AS (
            SELECT DISTINCT server_name
            FROM msdb.dbo.sysmanagement_shared_registered_servers_internal
        )
        SELECT 
            ISNULL(p.SQLVersion, 'Unknown') AS SQLVersion,
            COUNT(DISTINCT cms.server_name) AS TotalServers,
            SUM(CASE WHEN p.PatchStatus = 'Up to Date' THEN 1 ELSE 0 END) AS UpToDate,
            SUM(CASE WHEN p.PatchStatus <> 'Up to Date' OR p.PatchStatus IS NULL THEN 1 ELSE 0 END) AS NeedsUpdate,
            MAX(p.TargetCU) AS TargetCU,
            MAX(p.SupportEnds) AS EndOfSupport,
            MAX(p.SupportStatus) AS SupportStatus
        FROM CMS_Servers cms
        LEFT JOIN Patching.vw_PatchComplianceDetail p 
            ON UPPER(cms.server_name) = UPPER(p.ServerName)
        GROUP BY p.SQLVersion
        ORDER BY p.SQLVersion DESC;
    END
END;

GO

-- [report].[usp_UnifiedSecurityComplianceReport]

-- This is a large procedure - we'll add smart email logic at the send point
-- The procedure already uses config.ServerInventory which now syncs from CMS

-- Add a wrapper parameter and logic
CREATE PROCEDURE report.usp_UnifiedSecurityComplianceReport
    @ReportType NVARCHAR(20) = 'HOURLY',  -- HOURLY or DAILY
    @SendEmail BIT = 1,
    @EmailRecipients NVARCHAR(500) = 'SQLServerAdminNotification@YOURORG.gov',
    @Verbose BIT = 0,
    @OnlyOnIssues BIT = 1  -- NEW: Only send email if score < 85 or findings exist
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @ReportDate DATETIME2 = SYSDATETIME();
    DECLARE @LookbackHours INT = CASE WHEN @ReportType = 'HOURLY' THEN 1 ELSE 24 END;
    DECLARE @HTMLBody NVARCHAR(MAX) = '';
    DECLARE @Subject NVARCHAR(500) = '';
    DECLARE @StatusColor NVARCHAR(20) = '#28a745';
    
    -- Overall metrics
    DECLARE @OverallScore INT = 100;
    DECLARE @CJISScore INT = 100;
    DECLARE @NISTScore INT = 100;
    DECLARE @DISAScore INT = 100;
    DECLARE @TotalServers INT = 0;
    DECLARE @TotalLogins INT = 0;
    DECLARE @TotalSysAdmins INT = 0;
    DECLARE @TotalDatabases INT = 0;
    DECLARE @LoginFailures INT = 0;
    DECLARE @PermissionChanges INT = 0;
    DECLARE @DDLChanges INT = 0;
    DECLARE @ServiceAccountEvents INT = 0;
    DECLARE @CriticalFindings INT = 0;
    DECLARE @HighFindings INT = 0;
    DECLARE @MediumFindings INT = 0;
    DECLARE @DisabledLogins INT = 0;
    DECLARE @OrphanedUsers INT = 0;
    DECLARE @WeakPasswords INT = 0;
    DECLARE @ExcessivePrivileges INT = 0;
    DECLARE @PermissionDrift INT = 0;
    DECLARE @ProdServers INT = 0;
    DECLARE @NonProdServers INT = 0;
    
    -- ========================================================================
    -- GATHER METRICS - NOW USING CMS VIA config.vw_MonitoredServers
    -- ========================================================================
    
    -- Server counts from CMS (source of truth)
    SELECT @TotalServers = COUNT(DISTINCT cms.ServerName)
    FROM config.vw_MonitoredServers cms;
    
    -- Get environment breakdown from inventory (synced from CMS)
    SELECT 
        @ProdServers = ISNULL(SUM(CASE WHEN si.Environment = 'PROD' THEN 1 ELSE 0 END), 0),
        @NonProdServers = ISNULL(SUM(CASE WHEN si.Environment != 'PROD' OR si.Environment IS NULL THEN 1 ELSE 0 END), 0)
    FROM config.vw_MonitoredServers cms
    LEFT JOIN config.ServerInventory si 
        ON UPPER(cms.ServerName) = UPPER(si.ServerName) AND si.IsActive = 1;
    
    -- Login counts
    IF OBJECT_ID('security.LoginInventory', 'U') IS NOT NULL
    BEGIN
        SELECT @TotalLogins = ISNULL(COUNT(*), 0),
               @TotalSysAdmins = ISNULL(SUM(CASE WHEN IsSysAdmin = 1 THEN 1 ELSE 0 END), 0),
               @DisabledLogins = ISNULL(SUM(CASE WHEN IsDisabled = 1 THEN 1 ELSE 0 END), 0)
        FROM security.LoginInventory;
    END
    
    -- Database count
    IF OBJECT_ID('dbo.fact_DatabaseInventory', 'U') IS NOT NULL
        SELECT @TotalDatabases = ISNULL(COUNT(*), 0) FROM dbo.fact_DatabaseInventory;
    
    -- Login failures
    IF OBJECT_ID('audit.LoginFailures', 'U') IS NOT NULL
        SELECT @LoginFailures = ISNULL(COUNT(*), 0) FROM audit.LoginFailures 
        WHERE EventTime >= DATEADD(HOUR, -@LookbackHours, SYSDATETIME());
    
    -- Permission changes
    IF OBJECT_ID('audit.PermissionChanges', 'U') IS NOT NULL
        SELECT @PermissionChanges = ISNULL(COUNT(*), 0) FROM audit.PermissionChanges 
        WHERE ChangeTime >= DATEADD(HOUR, -@LookbackHours, SYSDATETIME());
    
    -- DDL changes
    IF OBJECT_ID('audit.ObjectChanges', 'U') IS NOT NULL
        SELECT @DDLChanges = ISNULL(COUNT(*), 0) FROM audit.ObjectChanges 
        WHERE EventTime >= DATEADD(HOUR, -@LookbackHours, SYSDATETIME());
    
    -- Permission drift
    IF OBJECT_ID('security.PermissionDrift', 'U') IS NOT NULL
        SELECT @PermissionDrift = ISNULL(COUNT(*), 0) FROM security.PermissionDrift WHERE IsResolved = 0;
    
    -- Service account events
    IF OBJECT_ID('security.LoginAudit', 'U') IS NOT NULL
        SELECT @ServiceAccountEvents = ISNULL(COUNT(*), 0) FROM security.LoginAudit
        WHERE LoginName LIKE '%mssqlserv%'
           AND EventTime >= DATEADD(HOUR, -@LookbackHours, SYSDATETIME());
    
    IF OBJECT_ID('audit.LoginFailures', 'U') IS NOT NULL
        SELECT @ServiceAccountEvents = @ServiceAccountEvents + ISNULL(COUNT(*), 0) 
        FROM audit.LoginFailures
        WHERE LoginName LIKE '%mssqlserv%'
           AND EventTime >= DATEADD(HOUR, -@LookbackHours, SYSDATETIME());
    
    -- ========================================================================
    -- COMPLIANCE SCORING (unchanged logic)
    -- ========================================================================
    
    SET @CJISScore = 100;
    IF @TotalSysAdmins > 20 SET @CJISScore = @CJISScore - 15;
    IF @LoginFailures > 100 SET @CJISScore = @CJISScore - 10;
    IF @PermissionDrift > 100 SET @CJISScore = @CJISScore - 15;
    IF @DisabledLogins > 50 SET @CJISScore = @CJISScore - 5;
    
    SET @NISTScore = 100;
    IF @TotalSysAdmins > (@TotalLogins * 0.15) SET @NISTScore = @NISTScore - 20;
    IF @PermissionChanges > 20 SET @NISTScore = @NISTScore - 10;
    IF @DDLChanges > 500 SET @NISTScore = @NISTScore - 10;
    IF @PermissionDrift > 50 SET @NISTScore = @NISTScore - 15;
    
    SET @DISAScore = 100;
    IF @TotalSysAdmins > 10 SET @DISAScore = @DISAScore - 10;
    IF @LoginFailures > 50 SET @DISAScore = @DISAScore - 10;
    IF @ServiceAccountEvents > 0 SET @DISAScore = @DISAScore - 5;
    IF @PermissionDrift > 20 SET @DISAScore = @DISAScore - 15;
    
    IF @CJISScore < 0 SET @CJISScore = 0;
    IF @NISTScore < 0 SET @NISTScore = 0;
    IF @DISAScore < 0 SET @DISAScore = 0;
    
    SET @OverallScore = (@CJISScore + @NISTScore + @DISAScore) / 3;
    
    -- Count findings
    IF @TotalSysAdmins > 20 SET @CriticalFindings = @CriticalFindings + 1;
    IF @PermissionDrift > 100 SET @CriticalFindings = @CriticalFindings + 1;
    IF @LoginFailures > 100 SET @HighFindings = @HighFindings + 1;
    IF @PermissionChanges > 20 SET @MediumFindings = @MediumFindings + 1;
    
    -- =====================================================
    -- SMART EMAIL LOGIC: Only send if issues exist
    -- =====================================================
    IF @OnlyOnIssues = 1 AND @OverallScore >= 85 AND @CriticalFindings = 0 AND @HighFindings = 0
    BEGIN
        PRINT 'DBAOps | JB: Security score ' + CAST(@OverallScore AS VARCHAR(10)) + 
            '/100 (healthy). No critical/high findings. Email not sent.';
        PRINT 'Set @OnlyOnIssues = 0 to always send, or check dashboard for full report.';
        
        -- Still save to history
        IF OBJECT_ID('report.SecurityReportHistory', 'U') IS NOT NULL
        BEGIN
            INSERT INTO report.SecurityReportHistory (ReportType, OverallScore, CJISScore, NISTScore, DISAScore, 
                TotalServers, TotalLogins, TotalSysAdmins, LoginFailures, PermissionChanges, ServiceAccountEvents, CriticalFindings, ReportHTML)
            VALUES (@ReportType, @OverallScore, @CJISScore, @NISTScore, @DISAScore,
                @TotalServers, @TotalLogins, @TotalSysAdmins, @LoginFailures, @PermissionChanges, @ServiceAccountEvents, @CriticalFindings, 
                'Report not sent - score healthy');
        END;
        
        RETURN;
    END;
    
    -- Determine status color
    IF @OverallScore >= 85 SET @StatusColor = '#28a745';
    ELSE IF @OverallScore >= 70 SET @StatusColor = '#ffc107';
    ELSE SET @StatusColor = '#dc3545';
    
    -- ========================================================================
    -- BUILD HTML REPORT (abbreviated - same structure as original)
    -- ========================================================================
    
    SET @Subject = CASE WHEN @ReportType = 'HOURLY' THEN 'â° ' ELSE 'ðŸ“Š ' END 
        + 'DBAOps Security Report - Score: ' + CAST(@OverallScore AS VARCHAR) + '/100'
        + CASE WHEN @CriticalFindings > 0 THEN ' - CRITICAL FINDINGS' 
               WHEN @HighFindings > 0 THEN ' - HIGH FINDINGS'
               ELSE '' END
        + ' - ' + FORMAT(@ReportDate, 'MMM dd, yyyy HH:mm');
    
    -- Build HTML body (keeping existing formatting)
    SET @HTMLBody = '<!DOCTYPE html><html><head><style>
        body{font-family:Segoe UI,Arial,sans-serif;margin:0;padding:20px;background:#f0f2f5}
        .container{max-width:1000px;margin:0 auto;background:white;border-radius:12px;box-shadow:0 4px 20px rgba(0,0,0,0.1)}
        .header{background:linear-gradient(135deg,' + @StatusColor + ',#1a1a2e);color:white;padding:30px;text-align:center;border-radius:12px 12px 0 0}
        .header h1{margin:0;font-size:52px}
        .header h2{margin:10px 0 0 0;font-weight:normal;opacity:0.95}
        .metrics{display:flex;flex-wrap:wrap;padding:20px;justify-content:center;gap:10px}
        .metric{text-align:center;padding:15px 20px;background:#f8f9fa;border-radius:8px;min-width:100px}
        .metric-value{font-size:24px;font-weight:bold;color:#333}
        .metric-label{font-size:10px;color:#666;text-transform:uppercase}
        .footer{background:#1a1a2e;color:white;padding:20px;text-align:center;font-size:12px;border-radius:0 0 12px 12px}
        </style></head><body><div class="container">';
    
    SET @HTMLBody = @HTMLBody + '<div class="header">';
    SET @HTMLBody = @HTMLBody + '<h1>' + CAST(@OverallScore AS VARCHAR) + '/100</h1>';
    SET @HTMLBody = @HTMLBody + '<h2>Security & Compliance Score</h2>';
    SET @HTMLBody = @HTMLBody + '<p style="opacity:0.8">' + @ReportType + ' Report | ' + FORMAT(@ReportDate, 'MMM dd, yyyy h:mm tt') + '</p>';
    SET @HTMLBody = @HTMLBody + '<p style="opacity:0.7">Source: CMS (' + CAST(@TotalServers AS VARCHAR) + ' servers)</p>';
    SET @HTMLBody = @HTMLBody + '</div>';
    
    -- Metrics bar
    SET @HTMLBody = @HTMLBody + '<div class="metrics">';
    SET @HTMLBody = @HTMLBody + '<div class="metric"><div class="metric-value">' + CAST(@CJISScore AS VARCHAR) + '%</div><div class="metric-label">CJIS</div></div>';
    SET @HTMLBody = @HTMLBody + '<div class="metric"><div class="metric-value">' + CAST(@NISTScore AS VARCHAR) + '%</div><div class="metric-label">NIST</div></div>';
    SET @HTMLBody = @HTMLBody + '<div class="metric"><div class="metric-value">' + CAST(@DISAScore AS VARCHAR) + '%</div><div class="metric-label">DISA</div></div>';
    SET @HTMLBody = @HTMLBody + '<div class="metric"><div class="metric-value">' + CAST(@TotalSysAdmins AS VARCHAR) + '</div><div class="metric-label">SysAdmins</div></div>';
    SET @HTMLBody = @HTMLBody + '<div class="metric"><div class="metric-value">' + CAST(@LoginFailures AS VARCHAR) + '</div><div class="metric-label">Login Fails</div></div>';
    SET @HTMLBody = @HTMLBody + '<div class="metric"><div class="metric-value">' + CAST(@PermissionDrift AS VARCHAR) + '</div><div class="metric-label">Perm Drift</div></div>';
    SET @HTMLBody = @HTMLBody + '</div>';
    
    -- Critical findings
    IF @CriticalFindings > 0 OR @HighFindings > 0
    BEGIN
        SET @HTMLBody = @HTMLBody + '<div style="padding:20px;background:#f8d7da;margin:20px;border-radius:8px;border-left:4px solid #721c24">';
        SET @HTMLBody = @HTMLBody + '<h3 style="margin:0 0 10px 0;color:#721c24">ðŸš¨ Findings Requiring Attention</h3>';
        IF @TotalSysAdmins > 20
            SET @HTMLBody = @HTMLBody + '<p style="margin:5px 0"><strong>CRITICAL:</strong> ' + CAST(@TotalSysAdmins AS VARCHAR) + ' SysAdmin accounts (limit: 20)</p>';
        IF @PermissionDrift > 100
            SET @HTMLBody = @HTMLBody + '<p style="margin:5px 0"><strong>CRITICAL:</strong> ' + CAST(@PermissionDrift AS VARCHAR) + ' unresolved permission drift</p>';
        IF @LoginFailures > 100
            SET @HTMLBody = @HTMLBody + '<p style="margin:5px 0"><strong>HIGH:</strong> ' + CAST(@LoginFailures AS VARCHAR) + ' login failures in ' + CAST(@LookbackHours AS VARCHAR) + 'h</p>';
        SET @HTMLBody = @HTMLBody + '</div>';
    END;
    
    -- Footer
    SET @HTMLBody = @HTMLBody + '<div class="footer">';
    SET @HTMLBody = @HTMLBody + '<strong>DBAOps Security & Compliance Report</strong><br>';
    SET @HTMLBody = @HTMLBody + 'This report only sent when score &lt; 85 or findings exist. Full details in dashboard.<br>';
    SET @HTMLBody = @HTMLBody + 'John Bosco RHOYLLE INNOVATION EMPIRE™, Sr. SQL DBA | YourState DPS';
    SET @HTMLBody = @HTMLBody + '</div></div></body></html>';
    
    -- Save to history
    IF OBJECT_ID('report.SecurityReportHistory', 'U') IS NOT NULL
    BEGIN
        INSERT INTO report.SecurityReportHistory (ReportType, OverallScore, CJISScore, NISTScore, DISAScore, 
            TotalServers, TotalLogins, TotalSysAdmins, LoginFailures, PermissionChanges, ServiceAccountEvents, CriticalFindings, ReportHTML)
        VALUES (@ReportType, @OverallScore, @CJISScore, @NISTScore, @DISAScore,
            @TotalServers, @TotalLogins, @TotalSysAdmins, @LoginFailures, @PermissionChanges, @ServiceAccountEvents, @CriticalFindings, @HTMLBody);
    END;
    
    -- Send email
    IF @SendEmail = 1
    BEGIN
        EXEC msdb.dbo.sp_send_dbmail 
            @profile_name = 'DBAOpsProfile', 
            @recipients = @EmailRecipients,
            @subject = @Subject, 
            @body = @HTMLBody, 
            @body_format = 'HTML';
        PRINT 'DBAOps | JB: Security Report sent - Score: ' + CAST(@OverallScore AS VARCHAR) + '/100';
    END;
    
    IF @Verbose = 1
    BEGIN
        SELECT 
            @OverallScore AS OverallScore,
            @CJISScore AS CJIS,
            @NISTScore AS NIST,
            @DISAScore AS DISA,
            @CriticalFindings AS Critical,
            @HighFindings AS High,
            @TotalServers AS Servers,
            @TotalSysAdmins AS SysAdmins;
    END;
    
    PRINT @ReportType + ' Security Report Complete - Score: ' + CAST(@OverallScore AS VARCHAR) + 
        '/100 (CJIS:' + CAST(@CJISScore AS VARCHAR) + ' NIST:' + CAST(@NISTScore AS VARCHAR) + 
        ' DISA:' + CAST(@DISAScore AS VARCHAR) + ')';
END;

GO

-- [rpt].[usp_DailyOneSentenceReport]

-- Create or alter the procedure
CREATE   PROCEDURE rpt.usp_DailyOneSentenceReport
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @TotalServers INT;
    DECLARE @CompliantServers INT;
    DECLARE @CompliancePercent DECIMAL(5,2);
    DECLARE @CriticalCount INT;
    DECLARE @Status VARCHAR(20);
    DECLARE @Report NVARCHAR(500);
    
    -- Get metrics
    SELECT 
        @TotalServers = COUNT(DISTINCT ServerName),
        @CompliantServers = COUNT(DISTINCT CASE WHEN IsCompliant = 1 THEN ServerName END),
        @CriticalCount = COUNT(DISTINCT CASE WHEN Status = 'Critical' THEN ServerName END)
    FROM ctl.BackupCompliance;
    
    SET @CompliancePercent = CAST(@CompliantServers AS DECIMAL(5,2)) / NULLIF(@TotalServers, 0) * 100;
    
    -- Determine status
    SET @Status = CASE 
        WHEN @CompliancePercent >= 95 THEN 'HEALTHY'
        WHEN @CompliancePercent >= 80 THEN 'WARNING'
        ELSE 'CRITICAL'
    END;
    
    -- Build one-sentence report
    SET @Report = FORMAT(GETDATE(), 'MMM dd, yyyy') + ': ' +
        @Status + ' - ' +
        CAST(@CompliantServers AS VARCHAR) + ' of ' + CAST(@TotalServers AS VARCHAR) + 
        ' servers compliant (' + CAST(@CompliancePercent AS VARCHAR) + '%)' +
        CASE WHEN @CriticalCount > 0 
            THEN ', ' + CAST(@CriticalCount AS VARCHAR) + ' critical'
            ELSE ''
        END + '.';
    
    SELECT @Report AS DailyBackupReport;
END;

GO

-- [rpt].[usp_Email_ReplicationReport]

CREATE PROCEDURE rpt.usp_Email_ReplicationReport
    @SendAlways BIT = 0,
    @ProfileName NVARCHAR(128) = NULL,  -- Will auto-detect if NULL
    @Recipients NVARCHAR(MAX) = 'SQLAdmins@YOURORG.GOV;SQLServerAdminPersonnel@YOURORG.GOV;SQLServerAdminNotification@YOURORG.gov'
AS
BEGIN
    SET NOCOUNT ON;
    
    -- Auto-detect working profile if not specified
    IF @ProfileName IS NULL
    BEGIN
        -- Use profile_id 2 which is confirmed working
        SELECT @ProfileName = name FROM msdb.dbo.sysmail_profile WHERE profile_id = 2;
        
        -- Fallback to default if not found
        IF @ProfileName IS NULL
        BEGIN
            SELECT TOP 1 @ProfileName = p.name 
            FROM msdb.dbo.sysmail_principalprofile pp
            JOIN msdb.dbo.sysmail_profile p ON pp.profile_id = p.profile_id
            WHERE pp.is_default = 1;
        END
    END
    
    DECLARE @Subject NVARCHAR(255);
    DECLARE @Body NVARCHAR(MAX);
    DECLARE @HealthyCount INT;
    DECLARE @UnhealthyCount INT;
    DECLARE @TotalCount INT;
    DECLARE @HealthPct DECIMAL(5,2);
    DECLARE @HasIssues BIT = 0;
    
    -- Get health summary
    SELECT 
        @TotalCount = COUNT(DISTINCT PublisherServer),
        @HealthyCount = SUM(CASE WHEN Status = 'Healthy' THEN 1 ELSE 0 END),
        @UnhealthyCount = SUM(CASE WHEN Status != 'Healthy' THEN 1 ELSE 0 END)
    FROM ctl.ReplicationHealth;
    
    SET @HealthPct = CASE WHEN @TotalCount > 0 THEN (@HealthyCount * 100.0 / @TotalCount) ELSE 0 END;
    SET @HasIssues = CASE WHEN @UnhealthyCount > 0 THEN 1 ELSE 0 END;
    
    -- Build subject line
    SET @Subject = CASE 
        WHEN @UnhealthyCount > 0 THEN 'ALERT: DBAOps Replication Issues - ' + CAST(@UnhealthyCount AS VARCHAR) + ' Publisher(s) Unhealthy'
        ELSE 'DBAOps Replication Health Report - All ' + CAST(@TotalCount AS VARCHAR) + ' Publishers Healthy'
    END;
    
    -- Build HTML email body
    SET @Body = '
<!DOCTYPE html>
<html>
<head>
    <style>
        body { font-family: Segoe UI, Arial, sans-serif; margin: 20px; }
        .header { background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%); color: white; padding: 20px; border-radius: 8px 8px 0 0; }
        .header h1 { margin: 0; font-size: 24px; }
        .header p { margin: 5px 0 0 0; opacity: 0.9; }
        .content { background: #f8f9fa; padding: 20px; border: 1px solid #dee2e6; }
        .summary-box { background: white; border-radius: 8px; padding: 15px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
        .metric { display: inline-block; text-align: center; padding: 10px 20px; }
        .metric-value { font-size: 32px; font-weight: bold; }
        .metric-label { font-size: 12px; color: #6c757d; text-transform: uppercase; }
        .healthy { color: #28a745; }
        .unhealthy { color: #dc3545; }
        .warning { color: #ffc107; }
        table { width: 100%; border-collapse: collapse; background: white; margin-top: 15px; }
        th { background: #343a40; color: white; padding: 12px; text-align: left; }
        td { padding: 10px 12px; border-bottom: 1px solid #dee2e6; }
        tr:hover { background: #f1f3f5; }
        .status-healthy { color: #28a745; font-weight: bold; }
        .status-unhealthy { color: #dc3545; font-weight: bold; }
        .status-warning { color: #ffc107; font-weight: bold; }
        .footer { background: #343a40; color: #adb5bd; padding: 15px 20px; font-size: 12px; border-radius: 0 0 8px 8px; }
        .threshold-info { background: #e9ecef; padding: 10px 15px; border-radius: 4px; margin-top: 15px; font-size: 13px; }
    </style>
</head>
<body>
    <div class="header">
        <h1>DBAOps Replication Health Report</h1>
        <p>Generated: ' + FORMAT(GETDATE(), 'MMMM dd, yyyy h:mm tt') + '</p>
    </div>
    
    <div class="content">
        <div class="summary-box">
            <div class="metric">
                <div class="metric-value">' + CAST(@TotalCount AS VARCHAR) + '</div>
                <div class="metric-label">Publishers</div>
            </div>
            <div class="metric">
                <div class="metric-value healthy">' + CAST(@HealthyCount AS VARCHAR) + '</div>
                <div class="metric-label">Healthy</div>
            </div>
            <div class="metric">
                <div class="metric-value ' + CASE WHEN @UnhealthyCount > 0 THEN 'unhealthy' ELSE '' END + '">' + CAST(@UnhealthyCount AS VARCHAR) + '</div>
                <div class="metric-label">Unhealthy</div>
            </div>
            <div class="metric">
                <div class="metric-value ' + CASE WHEN @HealthPct = 100 THEN 'healthy' WHEN @HealthPct >= 80 THEN 'warning' ELSE 'unhealthy' END + '">' + CAST(CAST(@HealthPct AS DECIMAL(5,1)) AS VARCHAR) + '%</div>
                <div class="metric-label">Health Score</div>
            </div>
        </div>
        
        <h3>Publisher Status Details</h3>
        <table>
            <tr>
                <th>Publisher</th>
                <th>Status</th>
                <th>Details</th>
                <th>Last Check</th>
            </tr>';
    
    -- Add publisher details
    SELECT @Body = @Body + '
            <tr>
                <td><strong>' + h.PublisherServer + '</strong></td>
                <td class="status-' + LOWER(h.Status) + '">' + h.Status + '</td>
                <td>' + ISNULL(h.StatusMessage, '') + '</td>
                <td>' + FORMAT(h.CollectionTime, 'MM/dd/yyyy HH:mm') + '</td>
            </tr>'
    FROM ctl.ReplicationHealth h
    ORDER BY 
        CASE h.Status WHEN 'Healthy' THEN 2 WHEN 'Warning' THEN 1 ELSE 0 END,
        h.PublisherServer;
    
    SET @Body = @Body + '
        </table>
        
        <div class="threshold-info">
            <strong>Monitoring Thresholds:</strong> Latency: 600 seconds (10 min) | Pending Commands: 10,000
        </div>
        
        <h3>Replication Topology Summary</h3>
        <table>
            <tr>
                <th>Publisher</th>
                <th>Publisher DB</th>
                <th>Subscriber</th>
                <th>Subscriber DB</th>
                <th>Publications</th>
                <th>Articles</th>
            </tr>';
    
    -- Add topology details
    SELECT @Body = @Body + '
            <tr>
                <td>' + PublisherServer + '</td>
                <td>' + PublisherDatabase + '</td>
                <td>' + SubscriberServer + '</td>
                <td>' + SubscriberDatabase + '</td>
                <td style="text-align:center;">' + CAST(COUNT(*) AS VARCHAR) + '</td>
                <td style="text-align:center;">' + CAST(SUM(ISNULL(ArticleCount, 1)) AS VARCHAR) + '</td>
            </tr>'
    FROM config.ReplicationPublications
    WHERE IsActive = 1
    GROUP BY PublisherServer, PublisherDatabase, SubscriberServer, SubscriberDatabase
    ORDER BY PublisherServer, PublisherDatabase;
    
    SET @Body = @Body + '
        </table>
    </div>
    
    <div class="footer">
        <strong>DBAOps Monitoring Framework</strong> | Server: ' + @@SERVERNAME + '<br>
        Author: John Bosco RHOYLLE INNOVATION EMPIRE™, Sr SQL DBA/DBAOps Engineer, Microsoft Certified<br>
        This is an automated report. Do not reply to this email.
    </div>
</body>
</html>';
    
    -- Send email if there are issues OR if @SendAlways = 1
    IF @HasIssues = 1 OR @SendAlways = 1
    BEGIN
        EXEC msdb.dbo.sp_send_dbmail
            @profile_name = @ProfileName,
            @recipients = @Recipients,
            @subject = @Subject,
            @body = @Body,
            @body_format = 'HTML';
            
        PRINT 'Email sent using profile: ' + @ProfileName;
        PRINT 'Recipients: ' + @Recipients;
    END
    ELSE
    BEGIN
        PRINT 'No issues detected. Email not sent (use @SendAlways=1 to force send).';
    END
END

GO

-- [rpt].[usp_InfrastructureHealthReport]

CREATE PROCEDURE rpt.usp_InfrastructureHealthReport
/*
================================================================================
   DBAOps Monitoring and Compliance Framework
   Author: John Bosco RHOYLLE INNOVATION EMPIRE™, Sr SQL DBA/DBAOps Engineer
   
   Comprehensive Infrastructure Health Report
================================================================================
*/
    @SendEmail BIT = 0,
    @Recipients NVARCHAR(MAX) = 'DBAOps@YOURORG.gov'
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @ServerCount INT = 0;
    DECLARE @DatabaseCount INT = 0;
    DECLARE @NonCompliantFiles INT = 0;
    DECLARE @SysAdminCount INT = 0;
    DECLARE @RecentChanges INT = 0;
    DECLARE @FailedBackups INT = 0;
    DECLARE @Body NVARCHAR(MAX);
    DECLARE @Subject NVARCHAR(255);
    DECLARE @ProfileName NVARCHAR(128);
    
    -- Get server count from config.ServerReachability (ALL servers, not just reachable)
    SELECT @ServerCount = COUNT(DISTINCT ServerName) FROM config.ServerReachability;
    
    -- Get database count
    IF OBJECT_ID('inventory.DatabaseDetails', 'U') IS NOT NULL
        SELECT @DatabaseCount = COUNT(*) FROM inventory.DatabaseDetails 
        WHERE CollectedAt >= DATEADD(DAY, -1, GETDATE());
    
    IF @DatabaseCount = 0 AND OBJECT_ID('dbo.vw_LatestDatabaseInventory', 'V') IS NOT NULL
        SELECT @DatabaseCount = COUNT(*) FROM dbo.vw_LatestDatabaseInventory;
    
    -- Get non-compliant files
    IF OBJECT_ID('inventory.FileGrowthConfig', 'U') IS NOT NULL
        SELECT @NonCompliantFiles = COUNT(*) FROM inventory.FileGrowthConfig 
        WHERE IsCompliant = 0 AND CollectedAt >= DATEADD(DAY, -1, GETDATE());
    
    -- Get sysadmin count
    IF OBJECT_ID('security.LoginInventory', 'U') IS NOT NULL
        SELECT @SysAdminCount = COUNT(DISTINCT LoginName) FROM security.LoginInventory 
        WHERE IsSysAdmin = 1 AND CollectedAt >= DATEADD(DAY, -1, GETDATE());
    
    -- Get recent security changes
    IF OBJECT_ID('rpt.vw_SecurityChangesAudit', 'V') IS NOT NULL
        SELECT @RecentChanges = COUNT(*) FROM rpt.vw_SecurityChangesAudit 
        WHERE EventTime >= DATEADD(DAY, -7, GETDATE());
    
    -- Get failed backups
    IF OBJECT_ID('ctl.BackupCompliance', 'U') IS NOT NULL
        SELECT @FailedBackups = COUNT(*) FROM ctl.BackupCompliance 
        WHERE IsCompliant = 0 AND CollectionTime >= DATEADD(DAY, -1, GETDATE());
    
    -- Build report
    PRINT '================================================================================';
    PRINT '  *** DBAOps Infrastructure Health Report ***';
    PRINT '  Generated: ' + CONVERT(VARCHAR, GETDATE(), 120);
    PRINT '  Server: ' + @@SERVERNAME;
    PRINT '================================================================================';
    PRINT '';
    PRINT 'SUMMARY';
    PRINT '-------';
    PRINT '  Monitored Servers: ' + CAST(@ServerCount AS VARCHAR);
    PRINT '  Total Databases: ' + CAST(@DatabaseCount AS VARCHAR);
    PRINT '  SysAdmin Logins: ' + CAST(@SysAdminCount AS VARCHAR);
    PRINT '';
    PRINT 'COMPLIANCE';
    PRINT '----------';
    PRINT '  File Growth Non-Compliant: ' + CAST(@NonCompliantFiles AS VARCHAR);
    PRINT '  Failed Backups (24h): ' + CAST(@FailedBackups AS VARCHAR);
    PRINT '';
    PRINT 'SECURITY';
    PRINT '--------';
    PRINT '  Security Changes (7 days): ' + CAST(@RecentChanges AS VARCHAR);
    
    -- Email if requested
    IF @SendEmail = 1
    BEGIN
        SET @Subject = '*** DBAOps Infrastructure Health Report - ' + CONVERT(VARCHAR, GETDATE(), 107) + ' ***';
        SET @Body = 'DBAOps Infrastructure Health Report' + CHAR(13) + CHAR(10) +
                    '===================================' + CHAR(13) + CHAR(10) +
                    'Generated: ' + CONVERT(VARCHAR, GETDATE(), 120) + CHAR(13) + CHAR(10) +
                    'Server: ' + @@SERVERNAME + CHAR(13) + CHAR(10) + CHAR(13) + CHAR(10) +
                    'SUMMARY' + CHAR(13) + CHAR(10) +
                    '  Monitored Servers: ' + CAST(@ServerCount AS VARCHAR) + CHAR(13) + CHAR(10) +
                    '  Total Databases: ' + CAST(@DatabaseCount AS VARCHAR) + CHAR(13) + CHAR(10) +
                    '  SysAdmin Logins: ' + CAST(@SysAdminCount AS VARCHAR) + CHAR(13) + CHAR(10) + CHAR(13) + CHAR(10) +
                    'COMPLIANCE' + CHAR(13) + CHAR(10) +
                    '  File Growth Non-Compliant: ' + CAST(@NonCompliantFiles AS VARCHAR) + CHAR(13) + CHAR(10) +
                    '  Failed Backups (24h): ' + CAST(@FailedBackups AS VARCHAR) + CHAR(13) + CHAR(10) + CHAR(13) + CHAR(10) +
                    'SECURITY' + CHAR(13) + CHAR(10) +
                    '  Security Changes (7 days): ' + CAST(@RecentChanges AS VARCHAR);
        
        SELECT TOP 1 @ProfileName = name FROM msdb.dbo.sysmail_profile;
        IF @ProfileName IS NOT NULL
            EXEC msdb.dbo.sp_send_dbmail @profile_name = @ProfileName, @recipients = @Recipients,
                @subject = @Subject, @body = @Body;
    END
    
    -- Return summary
    SELECT 
        @ServerCount AS MonitoredServers,
        @DatabaseCount AS TotalDatabases,
        @SysAdminCount AS SysAdminLogins,
        @NonCompliantFiles AS NonCompliantFiles,
        @FailedBackups AS FailedBackups24h,
        @RecentChanges AS SecurityChanges7d;
END

GO

-- [rpt].[usp_ReplicationOneSentenceReport]

CREATE PROCEDURE rpt.usp_ReplicationOneSentenceReport
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @TotalPublishers INT;
    DECLARE @HealthyPublishers INT;
    DECLARE @HealthPct DECIMAL(5,2);
    DECLARE @Status NVARCHAR(20);
    DECLARE @Report NVARCHAR(500);
    
    -- Count rows (now should be 1 per publisher)
    SELECT @TotalPublishers = COUNT(*)
    FROM ctl.ReplicationHealth;
    
    SELECT @HealthyPublishers = COUNT(*)
    FROM ctl.ReplicationHealth
    WHERE Status = 'Healthy';
    
    IF @TotalPublishers > 0
        SET @HealthPct = (@HealthyPublishers * 100.0) / @TotalPublishers;
    ELSE
        SET @HealthPct = 0;
    
    IF @HealthPct >= 100
        SET @Status = 'HEALTHY';
    ELSE IF @HealthPct >= 80
        SET @Status = 'WARNING';
    ELSE
        SET @Status = 'CRITICAL';
    
    SET @Report = FORMAT(GETDATE(), 'MMM dd, yyyy') + ': ' + @Status + 
                  ' - ' + CAST(@HealthyPublishers AS VARCHAR) + ' of ' + 
                  CAST(@TotalPublishers AS VARCHAR) + ' publishers healthy (' + 
                  CAST(@HealthPct AS VARCHAR) + '%).';
    
    SELECT @Report AS ReplicationReport;
END

GO

-- [rpt].[usp_ReplicationSummary]

CREATE PROCEDURE rpt.usp_ReplicationSummary
AS
BEGIN
    SET NOCOUNT ON;
    
    -- First refresh the health data
    EXEC ctl.usp_Check_ReplicationHealth @Verbose = 0;
    
    -- Summary counts
    SELECT 
        COUNT(*) AS TotalPublishers,
        SUM(CASE WHEN Status = 'Healthy' THEN 1 ELSE 0 END) AS Healthy,
        SUM(CASE WHEN Status = 'Warning' THEN 1 ELSE 0 END) AS Warning,
        SUM(CASE WHEN Status = 'Critical' THEN 1 ELSE 0 END) AS Critical,
        SUM(CASE WHEN Status = 'Unknown' THEN 1 ELSE 0 END) AS Unknown,
        CAST(SUM(CASE WHEN IsHealthy = 1 THEN 1 ELSE 0 END) * 100.0 / COUNT(*) AS DECIMAL(5,2)) AS HealthyPct
    FROM ctl.ReplicationHealth;
    
    -- Detailed status
    SELECT 
        r.PublisherServer,
        r.ReplicationType,
        r.SubscriberServer,
        h.Status,
        h.StatusMessage,
        h.CollectionTime
    FROM config.ReplicationServers r
    LEFT JOIN ctl.ReplicationHealth h ON r.PublisherServer = h.PublisherServer
    WHERE r.IsActive = 1
    ORDER BY 
        CASE h.Status WHEN 'Critical' THEN 1 WHEN 'Warning' THEN 2 WHEN 'Unknown' THEN 3 ELSE 4 END,
        r.PublisherServer;
END

GO

-- [schemaDoc].[usp_LogDocumentation]

-- â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
-- 5. Create Procedure to Log Documentation
-- â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
CREATE   PROCEDURE schemaDoc.usp_LogDocumentation
    @ServerName NVARCHAR(128),
    @DatabaseName NVARCHAR(128),
    @TableCount INT = NULL,
    @ViewCount INT = NULL,
    @ProcedureCount INT = NULL,
    @FunctionCount INT = NULL,
    @ColumnCount INT = NULL,
    @RelationshipCount INT = NULL,
    @Status VARCHAR(20) = 'Success',
    @DurationSeconds INT = NULL,
    @OutputPath NVARCHAR(500) = NULL,
    @ErrorMessage NVARCHAR(MAX) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    INSERT INTO schemaDoc.DocumentationLog (
        ServerName, DatabaseName, TableCount, ViewCount, 
        ProcedureCount, FunctionCount, ColumnCount, RelationshipCount,
        Status, DurationSeconds, OutputPath, ErrorMessage
    )
    VALUES (
        @ServerName, @DatabaseName, @TableCount, @ViewCount,
        @ProcedureCount, @FunctionCount, @ColumnCount, @RelationshipCount,
        @Status, @DurationSeconds, @OutputPath, @ErrorMessage
    );
    
    RETURN SCOPE_IDENTITY();
END

GO

-- [security].[usp_CheckLoginDrift]

-- ============================================================================
-- PROCEDURE 2: Check Login Drift
-- ============================================================================

CREATE   PROCEDURE security.usp_CheckLoginDrift
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @DriftCount INT = 0;
    
    CREATE TABLE #LoginDrift (
        DriftType NVARCHAR(50),
        ServerName NVARCHAR(256),
        LoginName NVARCHAR(256),
        LoginType NVARCHAR(50),
        IsSysAdmin BIT,
        DetectedAt DATETIME2
    );
    
    -- New logins (in current, not in baseline)
    INSERT INTO #LoginDrift (DriftType, ServerName, LoginName, LoginType, IsSysAdmin, DetectedAt)
    SELECT 'NEW_LOGIN', li.ServerName, li.LoginName, li.LoginType, ISNULL(li.IsSysAdmin, 0), SYSDATETIME()
    FROM security.LoginInventory li
    WHERE NOT EXISTS (
        SELECT 1 FROM sec.SecurityBaseline sb
        WHERE sb.ServerName = li.ServerName AND sb.LoginName = li.LoginName
    );
    
    -- Removed logins (in baseline, not in current)
    INSERT INTO #LoginDrift (DriftType, ServerName, LoginName, LoginType, IsSysAdmin, DetectedAt)
    SELECT 'REMOVED_LOGIN', sb.ServerName, sb.LoginName, sb.TypeDesc, ISNULL(sb.HasSysAdmin, 0), SYSDATETIME()
    FROM sec.SecurityBaseline sb
    WHERE NOT EXISTS (
        SELECT 1 FROM security.LoginInventory li
        WHERE li.ServerName = sb.ServerName AND li.LoginName = sb.LoginName
    );
    
    -- SysAdmin status changed
    INSERT INTO #LoginDrift (DriftType, ServerName, LoginName, LoginType, IsSysAdmin, DetectedAt)
    SELECT 'SYSADMIN_CHANGED', li.ServerName, li.LoginName, li.LoginType, ISNULL(li.IsSysAdmin, 0), SYSDATETIME()
    FROM security.LoginInventory li
    INNER JOIN sec.SecurityBaseline sb ON li.ServerName = sb.ServerName AND li.LoginName = sb.LoginName
    WHERE ISNULL(li.IsSysAdmin, 0) != ISNULL(sb.HasSysAdmin, 0);
    
    SELECT @DriftCount = COUNT(*) FROM #LoginDrift;
    
    IF @DriftCount > 0
    BEGIN
        PRINT 'Login drift detected: ' + CAST(@DriftCount AS VARCHAR) + ' changes';
        SELECT * FROM #LoginDrift ORDER BY DriftType, ServerName, LoginName;
    END
    ELSE
        PRINT 'No login drift detected';
    
    DROP TABLE #LoginDrift;
END;

GO

-- [security].[usp_CheckPermissionDrift]

CREATE   PROCEDURE security.usp_CheckPermissionDrift
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @DriftCount INT = 0;
    
    IF OBJECT_ID('security.PermissionDrift', 'U') IS NOT NULL
    BEGIN
        SELECT @DriftCount = COUNT(*)
        FROM security.PermissionDrift
        WHERE DetectedDate >= DATEADD(HOUR, -1, SYSDATETIME())
          AND IsResolved = 0;
        
        IF @DriftCount > 0
        BEGIN
            PRINT 'Permission drift detected: ' + CAST(@DriftCount AS VARCHAR) + ' unresolved changes in last hour';
            
            SELECT TOP 20 
                ServerName, 
                ISNULL(DatabaseName, 'N/A') AS DatabaseName, 
                PrincipalName, 
                PermissionName, 
                DriftType, 
                DetectedDate,
                ISNULL(Notes, '') AS Notes
            FROM security.PermissionDrift
            WHERE DetectedDate >= DATEADD(HOUR, -1, SYSDATETIME())
              AND IsResolved = 0
            ORDER BY DetectedDate DESC;
        END
        ELSE
            PRINT 'No recent unresolved permission drift detected';
    END
    ELSE
        PRINT 'security.PermissionDrift table not found';
END;

GO

-- [security].[usp_Collect_SecurityInventory]

CREATE PROCEDURE security.usp_Collect_SecurityInventory
/*
================================================================================
   DBAOps Monitoring and Compliance Framework
   Author: John Bosco RHOYLLE INNOVATION EMPIRE™, Sr SQL DBA/DBAOps Engineer
   
   Collects comprehensive security inventory:
   - Server logins
   - Database users
   - Role memberships
   - Object permissions
================================================================================
*/
    @ServerName NVARCHAR(128) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    SET @ServerName = ISNULL(@ServerName, @@SERVERNAME);
    
    -- Collect Server Logins
    INSERT INTO security.LoginInventory (
        ServerName, LoginName, LoginType, IsDisabled, CreateDate, ModifyDate,
        DefaultDatabase, HasAccess, IsSysAdmin, IsSecurityAdmin, IsServerAdmin,
        IsSetupAdmin, IsProcessAdmin, IsDiskAdmin, IsBulkAdmin
    )
    SELECT 
        @ServerName,
        sp.name,
        sp.type_desc,
        sp.is_disabled,
        sp.create_date,
        sp.modify_date,
        sp.default_database_name,
        CASE WHEN sp.type IN ('S','U','G') THEN 1 ELSE 0 END,
        IS_SRVROLEMEMBER('sysadmin', sp.name),
        IS_SRVROLEMEMBER('securityadmin', sp.name),
        IS_SRVROLEMEMBER('serveradmin', sp.name),
        IS_SRVROLEMEMBER('setupadmin', sp.name),
        IS_SRVROLEMEMBER('processadmin', sp.name),
        IS_SRVROLEMEMBER('diskadmin', sp.name),
        IS_SRVROLEMEMBER('bulkadmin', sp.name)
    FROM sys.server_principals sp
    WHERE sp.type IN ('S', 'U', 'G')  -- SQL Login, Windows Login, Windows Group
    AND sp.name NOT LIKE '##%'
    AND sp.name NOT LIKE 'NT %';
    
    PRINT 'Collected ' + CAST(@@ROWCOUNT AS VARCHAR) + ' logins';
    
    -- Collect Server Role Memberships
    INSERT INTO security.RoleMembership (
        ServerName, DatabaseName, RoleName, MemberName, MemberType, RoleType
    )
    SELECT 
        @ServerName,
        NULL,
        r.name,
        m.name,
        m.type_desc,
        'Server'
    FROM sys.server_role_members srm
    JOIN sys.server_principals r ON srm.role_principal_id = r.principal_id
    JOIN sys.server_principals m ON srm.member_principal_id = m.principal_id;
    
    PRINT 'Collected ' + CAST(@@ROWCOUNT AS VARCHAR) + ' server role memberships';
END

GO

-- [security].[usp_Collect_SecurityInventory_AllServers]

CREATE PROCEDURE security.usp_Collect_SecurityInventory_AllServers
/*
================================================================================
   DBAOps Monitoring and Compliance Framework
   Author: John Bosco RHOYLLE INNOVATION EMPIRE™, Sr SQL DBA/DBAOps Engineer
   
   Collects security inventory from ALL servers in config.ServerReachability
================================================================================
*/
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @ServerName NVARCHAR(128);
    DECLARE @SQL NVARCHAR(MAX);
    DECLARE @Collected INT = 0;
    DECLARE @Errors INT = 0;
    DECLARE @TotalServers INT;
    
    -- Clear previous collection for today
    DELETE FROM security.LoginInventory 
    WHERE CAST(CollectedAt AS DATE) = CAST(GETDATE() AS DATE);
    
    SELECT @TotalServers = COUNT(DISTINCT ServerName) FROM config.ServerReachability;
    
    PRINT '================================================================================';
    PRINT '  Security Inventory Collection - All Servers';
    PRINT '  Started: ' + CONVERT(VARCHAR, GETDATE(), 120);
    PRINT '  Servers to process: ' + CAST(@TotalServers AS VARCHAR);
    PRINT '================================================================================';
    PRINT '';
    
    DECLARE server_cursor CURSOR FOR
    SELECT DISTINCT ServerName 
    FROM config.ServerReachability
    WHERE ServerName IS NOT NULL
    ORDER BY ServerName;
    
    OPEN server_cursor;
    FETCH NEXT FROM server_cursor INTO @ServerName;
    
    WHILE @@FETCH_STATUS = 0
    BEGIN
        BEGIN TRY
            -- Collect Logins
            SET @SQL = N'
            INSERT INTO security.LoginInventory (
                ServerName, LoginName, LoginType, IsDisabled, CreateDate, ModifyDate,
                DefaultDatabase, HasAccess, IsSysAdmin, CollectedAt
            )
            SELECT 
                ''' + @ServerName + N''',
                p.name,
                p.type_desc,
                p.is_disabled,
                p.create_date,
                p.modify_date,
                p.default_database_name,
                1,
                CASE WHEN IS_SRVROLEMEMBER(''sysadmin'', p.name) = 1 THEN 1 ELSE 0 END,
                GETDATE()
            FROM [' + @ServerName + N'].master.sys.server_principals p
            WHERE p.type IN (''S'', ''U'', ''G'')
            AND p.name NOT LIKE ''##%''
            AND p.name NOT LIKE ''NT SERVICE%''
            AND p.name NOT LIKE ''NT AUTHORITY%''';
            
            EXEC sp_executesql @SQL;
            SET @Collected = @Collected + 1;
            PRINT '  [OK] ' + @ServerName + ' (' + CAST(@Collected AS VARCHAR) + '/' + CAST(@TotalServers AS VARCHAR) + ')';
        END TRY
        BEGIN CATCH
            SET @Errors = @Errors + 1;
            PRINT '  [ERROR] ' + @ServerName + ': ' + LEFT(ERROR_MESSAGE(), 100);
        END CATCH
        
        FETCH NEXT FROM server_cursor INTO @ServerName;
    END
    
    CLOSE server_cursor;
    DEALLOCATE server_cursor;
    
    PRINT '';
    PRINT '================================================================================';
    PRINT '  Complete: ' + CAST(@Collected AS VARCHAR) + ' servers collected, ' + CAST(@Errors AS VARCHAR) + ' errors';
    PRINT '================================================================================';
END

GO

-- [security].[usp_CollectMissingLocalAdmins]
CREATE OR ALTER PROCEDURE security.usp_CollectMissingLocalAdmins
AS
BEGIN
    INSERT INTO security.MissingLocalAdminCompliance
    (
        ServerName,
        RequiredAccount,
        CollectedTime,
        IsMissing
    )
    SELECT
        ServerDescription,
        RequiredAccount,
        CollectedTime,
        IsMissing
    FROM security.vw_MissingLocalAdmins;
END

GO

-- [security].[usp_CollectPermissions]

CREATE PROCEDURE security.usp_CollectPermissions
    @ServerName NVARCHAR(256) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @SQL NVARCHAR(MAX);
    DECLARE @LinkedServer NVARCHAR(256);
    DECLARE @ErrorMsg NVARCHAR(MAX);
    
    -- Log start
    INSERT INTO ctl.ETLExecutionLog (PackageName, StartTime, Status)
    VALUES ('security.usp_CollectPermissions', GETDATE(), 'Running');
    
    DECLARE @ExecutionID BIGINT = SCOPE_IDENTITY();
    
    BEGIN TRY
        -- Cursor through all enabled servers
        DECLARE server_cursor CURSOR LOCAL FAST_FORWARD FOR
            SELECT ServerName 
            FROM config.ServerInventory 
            WHERE IsActive = 1 
              AND (@ServerName IS NULL OR ServerName = @ServerName);
        
        OPEN server_cursor;
        FETCH NEXT FROM server_cursor INTO @LinkedServer;
        
        WHILE @@FETCH_STATUS = 0
        BEGIN
            BEGIN TRY
                -- Collect Server-Level Permissions
                SET @SQL = N'
                INSERT INTO security.ServerPermissions (ServerName, PrincipalName, PrincipalType, PermissionName, PermissionState, CollectedDate)
                SELECT 
                    ''' + @LinkedServer + ''',
                    pr.name,
                    pr.type_desc,
                    pe.permission_name,
                    pe.state_desc,
                    GETDATE()
                FROM [' + @LinkedServer + '].master.sys.server_permissions pe
                JOIN [' + @LinkedServer + '].master.sys.server_principals pr ON pe.grantee_principal_id = pr.principal_id
                WHERE pr.type NOT IN (''R'')';
                
                EXEC sp_executesql @SQL;
                
                -- Collect Server Role Membership
                SET @SQL = N'
                INSERT INTO security.ServerPermissions (ServerName, PrincipalName, PrincipalType, PermissionName, PermissionState, CollectedDate)
                SELECT 
                    ''' + @LinkedServer + ''',
                    member.name,
                    member.type_desc,
                    ''MEMBER OF '' + role.name,
                    ''GRANT'',
                    GETDATE()
                FROM [' + @LinkedServer + '].master.sys.server_role_members rm
                JOIN [' + @LinkedServer + '].master.sys.server_principals role ON rm.role_principal_id = role.principal_id
                JOIN [' + @LinkedServer + '].master.sys.server_principals member ON rm.member_principal_id = member.principal_id';
                
                EXEC sp_executesql @SQL;
                
            END TRY
            BEGIN CATCH
                SET @ErrorMsg = 'Error collecting from ' + @LinkedServer + ': ' + ERROR_MESSAGE();
                RAISERROR(@ErrorMsg, 10, 1) WITH NOWAIT;
            END CATCH
            
            FETCH NEXT FROM server_cursor INTO @LinkedServer;
        END
        
        CLOSE server_cursor;
        DEALLOCATE server_cursor;
        
        -- Update execution log
        UPDATE ctl.ETLExecutionLog 
        SET EndTime = GETDATE(), Status = 'Success'
        WHERE ExecutionID = @ExecutionID;
        
    END TRY
    BEGIN CATCH
        UPDATE ctl.ETLExecutionLog 
        SET EndTime = GETDATE(), Status = 'Failed', ErrorMessage = ERROR_MESSAGE()
        WHERE ExecutionID = @ExecutionID;
        
        THROW;
    END CATCH
END

GO

-- [security].[usp_CollectServiceAccounts]

CREATE PROCEDURE security.usp_CollectServiceAccounts
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @SQL NVARCHAR(MAX);
    DECLARE @LinkedServer NVARCHAR(256);
    
    -- Clear previous data
    TRUNCATE TABLE security.ServiceAccounts;
    
    DECLARE server_cursor CURSOR LOCAL FAST_FORWARD FOR
        SELECT ServerName FROM config.ServerInventory WHERE IsActive = 1;
    
    OPEN server_cursor;
    FETCH NEXT FROM server_cursor INTO @LinkedServer;
    
    WHILE @@FETCH_STATUS = 0
    BEGIN
        BEGIN TRY
            SET @SQL = N'
            INSERT INTO security.ServiceAccounts (ServerName, ServiceName, AccountName, AccountType, Status, LastUpdated)
            SELECT 
                ''' + @LinkedServer + ''',
                servicename,
                service_account,
                CASE 
                    WHEN service_account LIKE ''NT SERVICE%'' THEN ''Virtual Service Account''
                    WHEN service_account LIKE ''NT AUTHORITY%'' THEN ''Built-in Account''
                    ELSE ''Domain Account''
                END,
                status_desc,
                GETDATE()
            FROM [' + @LinkedServer + '].master.sys.dm_server_services';
            
            EXEC sp_executesql @SQL;
        END TRY
        BEGIN CATCH
            -- Log error but continue
            PRINT 'Error collecting service accounts from ' + @LinkedServer + ': ' + ERROR_MESSAGE();
        END CATCH
        
        FETCH NEXT FROM server_cursor INTO @LinkedServer;
    END
    
    CLOSE server_cursor;
    DEALLOCATE server_cursor;
END

GO

-- [security].[usp_CollectSysadminMembership]
CREATE OR ALTER PROCEDURE security.usp_CollectSysadminMembership
AS
BEGIN
    SET NOCOUNT ON;
    DECLARE @ServerName NVARCHAR(128) = @@SERVERNAME;
    
    INSERT INTO security.SysadminMembership (ServerName, LoginName, LoginType, IsDisabled)
    SELECT @ServerName, sp.name, sp.type_desc, sp.is_disabled
    FROM sys.server_principals sp
    INNER JOIN sys.server_role_members srm ON sp.principal_id = srm.member_principal_id
    INNER JOIN sys.server_principals sr ON srm.role_principal_id = sr.principal_id
    WHERE sr.name = 'sysadmin' AND sp.type IN ('S', 'U', 'G');
END

GO

-- [security].[usp_CollectTDEStatus]
CREATE OR ALTER PROCEDURE security.usp_CollectTDEStatus
AS
BEGIN
    SET NOCOUNT ON;
    DECLARE @ServerName NVARCHAR(128) = @@SERVERNAME;
    
    INSERT INTO security.TDEStatus (ServerName, DatabaseName, EncryptionState, EncryptionStateName, CertificateName, CertificateExpiry)
    SELECT @ServerName, d.name, dek.encryption_state,
        CASE dek.encryption_state WHEN 0 THEN 'No key' WHEN 1 THEN 'Unencrypted' WHEN 2 THEN 'Encrypting' WHEN 3 THEN 'Encrypted' WHEN 4 THEN 'Key change' WHEN 5 THEN 'Decrypting' ELSE 'Unknown' END,
        c.name, c.expiry_date
    FROM sys.databases d
    LEFT JOIN sys.dm_database_encryption_keys dek ON d.database_id = dek.database_id
    LEFT JOIN sys.certificates c ON dek.encryptor_thumbprint = c.thumbprint
    WHERE d.database_id > 4;
END

GO

-- [security].[usp_ComplianceAuditReport]

CREATE PROCEDURE security.usp_ComplianceAuditReport
AS
BEGIN
    SET NOCOUNT ON;
    
    PRINT '================================================================';
    PRINT 'CJIS COMPLIANCE AUDIT REPORT';
    PRINT 'Generated: ' + CONVERT(VARCHAR(50), GETDATE(), 120);
    PRINT '================================================================';
    PRINT '';
    
    -- Summary by Tier
    PRINT '1. SERVERS BY COMPLIANCE TIER:';
    PRINT '-------------------------------';
    SELECT 
        ComplianceTier,
        ServerCount = COUNT(DISTINCT ServerName),
        HasCJI = MAX(CASE WHEN DataClassification LIKE '%CJI%' THEN 1 ELSE 0 END),
        HasPII = MAX(CASE WHEN DataClassification LIKE '%PII%' THEN 1 ELSE 0 END)
    FROM security.vw_ServerAccessControl
    GROUP BY ComplianceTier
    ORDER BY 
        CASE 
            WHEN ComplianceTier LIKE '%Tier 1%' THEN 1
            WHEN ComplianceTier LIKE '%Tier 2%' THEN 2
            WHEN ComplianceTier LIKE '%Tier 3%' THEN 3
            WHEN ComplianceTier LIKE '%Tier 4%' THEN 4
        END;
    
    PRINT '';
    PRINT '2. AD GROUP ACCESS SUMMARY:';
    PRINT '---------------------------';
    SELECT 
        ADGroupName,
        ServersAccessible = COUNT(DISTINCT ServerName),
        HighestTier = MIN(CASE 
            WHEN ComplianceTier LIKE '%Tier 1%' THEN 1
            WHEN ComplianceTier LIKE '%Tier 2%' THEN 2
            WHEN ComplianceTier LIKE '%Tier 3%' THEN 3
            WHEN ComplianceTier LIKE '%Tier 4%' THEN 4
        END),
        CJI_Access = MAX(CAST(CanAccessCJI AS INT)),
        PII_Access = MAX(CAST(CanAccessPII AS INT))
    FROM security.vw_ServerAccessControl
    GROUP BY ADGroupName
    ORDER BY HighestTier, ADGroupName;
    
    PRINT '';
    PRINT '3. CJIS HIGH SECURITY ACCESS (Tier 1):';
    PRINT '--------------------------------------';
    SELECT DISTINCT
        ServerName,
        DataClassification,
        ADGroupName,
        AccessLevel,
        RequiresBackground = CASE RequiresBackground WHEN 1 THEN 'FBI' ELSE 'None' END,
        RequiresMFA
    FROM security.vw_ServerAccessControl
    WHERE ComplianceTier = 'Tier 1 - CJIS High Security'
    ORDER BY ServerName, ADGroupName;
    
    PRINT '';
    PRINT '4. BACKGROUND CHECK REQUIREMENTS:';
    PRINT '---------------------------------';
    SELECT 
        ADGroupName,
        ComplianceTier,
        BackgroundCheckType = CASE 
            WHEN RequiresBackground = 1 AND ComplianceTier LIKE '%Tier 1%' THEN 'FBI Fingerprint'
            WHEN RequiresBackground = 1 THEN 'State Background'
            ELSE 'Not Required'
        END,
        ServersAffected = COUNT(DISTINCT ServerName)
    FROM security.vw_ServerAccessControl
    WHERE RequiresBackground = 1
    GROUP BY ADGroupName, ComplianceTier, RequiresBackground
    ORDER BY 
        CASE 
            WHEN ComplianceTier LIKE '%Tier 1%' THEN 1
            WHEN ComplianceTier LIKE '%Tier 2%' THEN 2
        END,
        ADGroupName;
    
    PRINT '';
    PRINT '================================================================';
    PRINT 'AUDIT COMPLETE';
    PRINT '================================================================';
END;

GO

-- [security].[usp_DetectOrphanedUsers]
CREATE OR ALTER PROCEDURE security.usp_DetectOrphanedUsers
AS
BEGIN
    SET NOCOUNT ON;
    PRINT 'Orphaned user detection - requires database context iteration';
END

GO

-- [security].[usp_DetectPermissionDrift]

CREATE PROCEDURE [security].[usp_DetectPermissionDrift]
AS
BEGIN
    SET NOCOUNT ON;

    --===========================================
    -- Step 1: Detect permission drift (new grants)
    --===========================================
    INSERT INTO security.PermissionDrift (
        ServerName,
        DatabaseName,
        PrincipalName,
        PermissionName,
        DriftType,
        DetectedDate
    )
    SELECT DISTINCT
        sp.ServerName,
        NULL,  -- assuming DB-level not used in this context
        sp.PrincipalName,
        sp.PermissionName,
        'Added',
        GETDATE()
    FROM security.ServerPermissions sp
    WHERE sp.CollectedDate > DATEADD(DAY, -1, GETDATE())
      AND NOT EXISTS (
          SELECT 1
          FROM security.PermissionBaseline pb
          WHERE pb.ServerName     = sp.ServerName
            AND pb.PrincipalName  = sp.PrincipalName
            AND pb.PermissionName = sp.PermissionName
            AND pb.PermissionScope = 'Server'
      )
      AND NOT EXISTS (
          SELECT 1
          FROM security.PermissionDrift pd
          WHERE pd.ServerName     = sp.ServerName
            AND pd.PrincipalName  = sp.PrincipalName
            AND pd.PermissionName = sp.PermissionName
            AND pd.IsResolved     = 0
      );

    --===========================================
    -- Step 2: Alert via Database Mail
    --===========================================
    DECLARE @DriftCount INT;

    SELECT @DriftCount = COUNT(*)
    FROM security.PermissionDrift
    WHERE IsResolved = 0
      AND DetectedDate > DATEADD(HOUR, -1, GETDATE());

    IF @DriftCount > 0
    BEGIN
        DECLARE @Subject NVARCHAR(256) = 'âš ï¸ ALERT: Permission Drift Detected - ' + CAST(@DriftCount AS NVARCHAR(10)) + ' new changes';
        DECLARE @Body NVARCHAR(MAX) = 
            'Permission drift has been detected in the last hour. Please review the [DBAOpsRepository].[security].[PermissionDrift] table for details.';

        EXEC msdb.dbo.sp_send_dbmail
            @profile_name = 'DBAOpsProfile',  -- âœ… CORRECTED PROFILE
            @recipients   = 'SQLServerAdminNotification@YOURORG.gov',
            @subject      = @Subject,
            @body         = @Body;
    END
END;

GO

-- [security].[usp_GetLocalAdminCompliance]

CREATE   PROCEDURE security.usp_GetLocalAdminCompliance
AS
BEGIN
    SET NOCOUNT ON;
    
    SELECT 
        cms.ServerName,
        COALESCE(cms.Environment, cms.CMSEnvironment, 'Unknown') AS Environment,
        cms.ComplianceTier
    FROM config.ServerInventory cms
    WHERE cms.IsActive = 1
    AND (cms.ServerType = 'SQL Server' OR cms.ServerType IS NULL)
    ORDER BY cms.Environment, cms.ServerName;
END

GO

-- [security].[usp_InsertLocalAdminMembership]

CREATE   PROCEDURE security.usp_InsertLocalAdminMembership
    @ServerName NVARCHAR(200),
    @AccountName NVARCHAR(200)
AS
BEGIN
    SET NOCOUNT ON;

    INSERT INTO security.LocalAdminMembership (ServerName, AccountName)
    VALUES (@ServerName, @AccountName);
END

GO

-- [security].[usp_RefreshSecurityBaseline]

-- ============================================================================
-- PROCEDURE 1: Refresh Security Baseline
-- ============================================================================

CREATE   PROCEDURE security.usp_RefreshSecurityBaseline
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @InsertedCount INT = 0;
    
    -- Insert new logins not in baseline
    INSERT INTO sec.SecurityBaseline (ServerName, LoginName, TypeDesc, DefaultDB, CreateDate, ModifyDate, HasSysAdmin, CollectedAt)
    SELECT 
        li.ServerName, 
        li.LoginName, 
        li.LoginType,
        li.DefaultDatabase,
        li.CreateDate,
        li.ModifyDate,
        ISNULL(li.IsSysAdmin, 0),
        SYSDATETIME()
    FROM security.LoginInventory li
    WHERE NOT EXISTS (
        SELECT 1 FROM sec.SecurityBaseline sb
        WHERE sb.ServerName = li.ServerName
          AND sb.LoginName = li.LoginName
    );
    
    SET @InsertedCount = @@ROWCOUNT;
    
    PRINT 'Security Baseline Refresh Complete - New logins added: ' + CAST(@InsertedCount AS VARCHAR);
END;

GO

-- [security].[usp_SendDBAOpsAlert]

-- ============================================
-- STEP 5: Create Alert Notification Procedure
-- ============================================
CREATE   PROCEDURE security.usp_SendDBAOpsAlert
    @ServerName NVARCHAR(128),
    @AlertSeverity VARCHAR(20),
    @AlertType VARCHAR(50),
    @Subject NVARCHAR(255),
    @Body NVARCHAR(MAX)
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @MailProfile NVARCHAR(200);
    DECLARE @Recipients NVARCHAR(MAX);
    DECLARE @MailItemID INT;
    
    BEGIN TRY
        -- Determine which mail profile and recipients to use
        SELECT TOP 1 
            @MailProfile = MailProfile,
            @Recipients = RecipientEmail
        FROM security.NotificationPreferences
        WHERE AlertSeverity = @AlertSeverity
          AND IsEnabled = 1
        ORDER BY PreferenceID;
        
        -- Default if no preference found
        IF @MailProfile IS NULL
        BEGIN
            SET @MailProfile = 'DBAOpsMail';
            SET @Recipients = 'dba-team@YOURORG.gov';
        END
        
        -- Add server name to subject
        SET @Subject = '[' + @ServerName + '] ' + @Subject;
        
        -- Send email
        EXEC msdb.dbo.sp_send_dbmail
            @profile_name = @MailProfile,
            @recipients = @Recipients,
            @subject = @Subject,
            @body = @Body,
            @body_format = 'HTML';
        
        -- Get the mail item ID
        SELECT TOP 1 @MailItemID = mailitem_id 
        FROM msdb.dbo.sysmail_mailitems
        WHERE subject = @Subject
        ORDER BY send_request_date DESC;
        
        -- Log the notification
        INSERT INTO security.NotificationLog 
            (ServerName, AlertSeverity, AlertType, Subject, Recipients, MailProfile, SentDate, MailItemID, SendStatus)
        VALUES 
            (@ServerName, @AlertSeverity, @AlertType, @Subject, @Recipients, @MailProfile, GETDATE(), @MailItemID, 'Sent');
        
        PRINT 'âœ“ Alert sent to ' + @Recipients + ' via ' + @MailProfile;
        
    END TRY
    BEGIN CATCH
        -- Log the error
        INSERT INTO security.NotificationLog 
            (ServerName, AlertSeverity, AlertType, Subject, Recipients, MailProfile, SentDate, SendStatus)
        VALUES 
            (@ServerName, @AlertSeverity, @AlertType, @Subject, @Recipients, @MailProfile, GETDATE(), 'Failed: ' + ERROR_MESSAGE());
        
        -- Re-raise the error
        THROW;
    END CATCH
END

GO

-- [security].[usp_SendLocalAdminAuditEmail]

CREATE PROCEDURE security.usp_SendLocalAdminAuditEmail
    @OnlyOnIssues BIT = 1  -- NEW: Only send if missing accounts
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @body NVARCHAR(MAX) = N'';
    DECLARE @html NVARCHAR(MAX);
    DECLARE @MissingCount INT = 0;
    
    -- Check for missing accounts
    ;WITH x AS (
        SELECT * FROM security.usp_GetLocalAdminCompliance()
    )
    SELECT @MissingCount = COUNT(*) FROM x;
    
    -- =====================================================
    -- SMART EMAIL LOGIC: Only send if missing accounts
    -- =====================================================
    IF @OnlyOnIssues = 1 AND @MissingCount = 0
    BEGIN
        PRINT 'DBAOps | JB: All Local Admin accounts compliant. Email not sent.';
        RETURN;
    END;
    
    ;WITH x AS (
        SELECT * FROM security.usp_GetLocalAdminCompliance()
    )
    SELECT @body = @body + 
        '<tr><td>' + ServerName + '</td><td>' + MissingAccount + '</td></tr>'
    FROM x;
    
    SET @html = '
    <h2>Local Admin Compliance Audit</h2>
    <p><strong>This report only sent when missing accounts exist.</strong></p>
    <p>The following servers are missing required Local Admin accounts:</p>
    <table border="1" cellpadding="6" cellspacing="0">
        <tr style="background-color:#f0f0f0;">
            <th>Server</th>
            <th>Missing Account</th>
        </tr>' + @body + '</table>';
    
    EXEC msdb.dbo.sp_send_dbmail
        @profile_name = 'DBAOpsProfile',
        @recipients = 'SQLServerAdminNotification@YOURORG.gov',
        @subject = '*** Local Admin Compliance Audit - Missing Accounts ***',
        @body = @html,
        @body_format = 'HTML';
    
    PRINT 'DBAOps | JB: Local admin audit sent - ' + CAST(@MissingCount AS VARCHAR) + ' missing';
END;

GO

-- [security].[usp_WhoHasAccessToServer]

CREATE PROCEDURE security.usp_WhoHasAccessToServer
    @ServerName NVARCHAR(128) = NULL,
    @ComplianceTier NVARCHAR(50) = NULL,
    @ADGroupName NVARCHAR(100) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    SELECT 
        ServerName,
        ComplianceTier,
        DataClassification,
        ADGroupName,
        AccessLevel,
        BackgroundCheck = CASE RequiresBackground WHEN 1 THEN 'Required' ELSE 'Not Required' END,
        MFA = CASE RequiresMFA WHEN 1 THEN 'Required' ELSE 'Not Required' END,
        CanAccessCJI,
        CanAccessPII,
        CanModifyData,
        AuditLevel
    FROM security.vw_ServerAccessControl
    WHERE (@ServerName IS NULL OR ServerName = @ServerName)
      AND (@ComplianceTier IS NULL OR ComplianceTier = @ComplianceTier)
      AND (@ADGroupName IS NULL OR ADGroupName = @ADGroupName)
    ORDER BY 
        ComplianceTier,
        ServerName,
        ADGroupName;
END;

GO

-- [TrackIT].[usp_CheckBackupHealth]

            CREATE PROCEDURE TrackIT.usp_CheckBackupHealth
            AS
            BEGIN
                -- Disabled: DBAOps relies exclusively on Veeam telemetry
                RETURN;
            END
        
GO

-- [TrackIT].[usp_CheckReplicationHealth]

-- Procedure 3: Check Replication Health
CREATE   PROCEDURE TrackIT.usp_CheckReplicationHealth
AS
BEGIN
    SET NOCOUNT ON;
    
    -- Check all Track-IT publications
    INSERT INTO TrackIT.ReplicationHealth (
        PublicationName,
        SubscriberServer,
        SubscriberDB,
        ReplicationLatency_Seconds,
        LatencyStatus,
        PendingCommands,
        PendingCommandsStatus,
        LogReaderState,
        LogReaderStatus,
        OverallReplicationStatus
    )
    SELECT 
        p.publication AS PublicationName,
        s.subscriber_server AS SubscriberServer,
        s.subscriber_db AS SubscriberDB,
        ISNULL(h.cur_latency, 0) AS ReplicationLatency_Seconds,
        CASE 
            WHEN ISNULL(h.cur_latency, 0) <= 60 THEN 'Healthy'
            WHEN ISNULL(h.cur_latency, 0) <= 300 THEN 'Warning'
            ELSE 'Critical'
        END AS LatencyStatus,
        ISNULL(h.pending_commands, 0) AS PendingCommands,
        CASE 
            WHEN ISNULL(h.pending_commands, 0) <= 10000 THEN 'Healthy'
            WHEN ISNULL(h.pending_commands, 0) <= 50000 THEN 'Warning'
            ELSE 'Critical'
        END AS PendingCommandsStatus,
        CASE WHEN j.enabled = 1 THEN 'Running' ELSE 'Stopped' END AS LogReaderState,
        CASE WHEN j.enabled = 1 THEN 'Healthy' ELSE 'Critical' END AS LogReaderStatus,
        CASE 
            WHEN j.enabled = 0 THEN 'Critical'
            WHEN ISNULL(h.cur_latency, 0) > 300 OR ISNULL(h.pending_commands, 0) > 50000 THEN 'Critical'
            WHEN ISNULL(h.cur_latency, 0) > 60 OR ISNULL(h.pending_commands, 0) > 10000 THEN 'Warning'
            ELSE 'Healthy'
        END AS OverallReplicationStatus
    FROM distribution.dbo.MSpublications p
    INNER JOIN distribution.dbo.MSsubscriptions s ON p.publication_id = s.publication_id
    LEFT JOIN distribution.dbo.MSdistribution_history h ON s.agent_id = h.agent_id
    LEFT JOIN msdb.dbo.sysjobs j ON 'REPL-LogReader' = j.name
    WHERE p.publisher_db = 'TRACKIT_DATA'
      AND h.time = (SELECT MAX(time) FROM distribution.dbo.MSdistribution_history WHERE agent_id = s.agent_id);
    
    -- Send alerts for critical replication issues
    IF EXISTS (
        SELECT 1 
        FROM TrackIT.ReplicationHealth 
        WHERE OverallReplicationStatus = 'Critical'
          AND CheckDateTime >= DATEADD(MINUTE, -5, GETDATE())
    )
    BEGIN
        EXEC TrackIT.usp_SendHealthAlert 
            @AlertType = 'Replication Health',
            @Severity = 'Critical',
            @Message = 'Track-IT replication is in CRITICAL state';
    END;
END;

GO

-- [TrackIT].[usp_MasterHealthCheck]

-- Procedure 5: Master Health Check (runs all checks)
CREATE   PROCEDURE TrackIT.usp_MasterHealthCheck
AS
BEGIN
    SET NOCOUNT ON;
    
    PRINT 'Starting Track-IT Master Health Check...';
    PRINT '';
    
    PRINT 'Checking database health...';
    EXEC TrackIT.usp_CheckDatabaseHealth;
    
    PRINT 'Checking backup health...';
    EXEC TrackIT.usp_CheckBackupHealth;
    
    PRINT 'Checking replication health...';
    EXEC TrackIT.usp_CheckReplicationHealth;
    
    PRINT '';
    PRINT 'Master Health Check completed!';
    PRINT 'Review TrackIT health tables for detailed results.';
END;

GO

-- [TrackIT].[usp_SendHealthAlert]

-- Procedure 4: Alert Sending (stub - requires DB Mail configuration)
CREATE   PROCEDURE TrackIT.usp_SendHealthAlert
    @AlertType NVARCHAR(100),
    @Severity NVARCHAR(20),
    @Message NVARCHAR(MAX)
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @EmailSubject NVARCHAR(500);
    DECLARE @EmailBody NVARCHAR(MAX);
    DECLARE @EmailRecipients NVARCHAR(MAX);
    
    -- Get alert configuration
    SELECT TOP 1
        @EmailSubject = EmailSubjectTemplate,
        @EmailBody = EmailBodyTemplate,
        @EmailRecipients = EmailRecipients
    FROM TrackIT.AlertConfiguration
    WHERE AlertCategory = @AlertType
      AND Severity = @Severity
      AND IsEnabled = 1;
    
    -- Log alert
    INSERT INTO TrackIT.AlertHistory (
        AlertName,
        Severity,
        AlertMessage,
        EmailRecipients
    )
    VALUES (
        @AlertType,
        @Severity,
        @Message,
        @EmailRecipients
    );
    
    -- Send email using DB Mail (requires configuration)
    IF @EmailRecipients IS NOT NULL
    BEGIN
        BEGIN TRY
            EXEC msdb.dbo.sp_send_dbmail
                @recipients = @EmailRecipients,
                @subject = @EmailSubject,
                @body = @EmailBody,
                @body_format = 'TEXT';
                
            UPDATE TrackIT.AlertHistory
            SET EmailSent = 1,
                EmailSentDateTime = GETDATE()
            WHERE AlertHistoryID = SCOPE_IDENTITY();
        END TRY
        BEGIN CATCH
            -- Log email failure but don't fail the procedure
            PRINT 'Email send failed: ' + ERROR_MESSAGE();
        END CATCH;
    END;
END;

GO

-- [TRACS].[usp_AnalyzeMultipleWindows]

-- Procedure 3: Analyze Multiple Windows
CREATE   PROCEDURE TRACS.usp_AnalyzeMultipleWindows
    @ServerName NVARCHAR(128) = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @Windows TABLE (WindowID INT, WindowName NVARCHAR(100), StartTime DATETIME, EndTime DATETIME);
    
    INSERT INTO @Windows VALUES
    (1, 'Dec 22 - 12:05 PM to 12:34 PM', '2025-12-22 12:05:00', '2025-12-22 12:34:00'),
    (2, 'Dec 23 - 9:30 AM to 9:36 AM', '2025-12-23 09:30:00', '2025-12-23 09:36:00'),
    (3, 'Dec 23 - 9:36 AM to 9:39 AM', '2025-12-23 09:36:00', '2025-12-23 09:39:00'),
    (4, 'Dec 23 - 8:47 PM to 8:52 PM', '2025-12-23 20:47:00', '2025-12-23 20:52:00');
    
    SELECT 
        w.WindowID, w.WindowName, p.ServerName,
        COUNT(*) AS Samples,
        CAST(AVG(p.SQLServerCPUPercent) AS DECIMAL(5,2)) AS AvgCPU,
        CAST(MAX(p.SQLServerCPUPercent) AS DECIMAL(5,2)) AS MaxCPU,
        CAST(AVG(p.MemoryUsedPercent) AS DECIMAL(5,2)) AS AvgMemory,
        SUM(p.BlockedSessions) AS BlockingEvents,
        CASE WHEN MAX(p.SQLServerCPUPercent) > 90 THEN 'âš ï¸ HIGH' WHEN MAX(p.SQLServerCPUPercent) > 70 THEN 'âš¡ MODERATE' ELSE 'âœ… NORMAL' END AS Assessment
    FROM @Windows w
    LEFT JOIN TRACS.PerformanceSnapshots p ON p.CaptureTime BETWEEN w.StartTime AND w.EndTime
        AND (@ServerName IS NULL OR p.ServerName = @ServerName) AND p.SQLServerCPUPercent >= 0
    GROUP BY w.WindowID, w.WindowName, p.ServerName
    ORDER BY w.WindowID;
END

GO

-- [TRACS].[usp_AnalyzeTimeWindow]

-- Procedure 2: Analyze Time Window
CREATE   PROCEDURE TRACS.usp_AnalyzeTimeWindow
    @ServerName NVARCHAR(128) = NULL,
    @StartTime DATETIME,
    @EndTime DATETIME
AS
BEGIN
    SET NOCOUNT ON;
    
    SELECT 
        ServerName,
        COUNT(*) AS SampleCount,
        CAST(AVG(SQLServerCPUPercent) AS DECIMAL(5,2)) AS AvgCPU,
        CAST(MAX(SQLServerCPUPercent) AS DECIMAL(5,2)) AS MaxCPU,
        CAST(AVG(MemoryUsedPercent) AS DECIMAL(5,2)) AS AvgMemory,
        CAST(AVG(PageLifeExpectancy) AS INT) AS AvgPLE,
        SUM(BlockedSessions) AS BlockingEvents,
        CAST(AVG(BatchRequestsPerSec) AS INT) AS AvgBatchReq,
        CASE WHEN MAX(SQLServerCPUPercent) > 90 THEN 'âš ï¸ HIGH' WHEN MAX(SQLServerCPUPercent) > 70 THEN 'âš¡ MODERATE' ELSE 'âœ… NORMAL' END AS Status
    FROM TRACS.PerformanceSnapshots
    WHERE CaptureTime BETWEEN @StartTime AND @EndTime
    AND (@ServerName IS NULL OR ServerName = @ServerName)
    AND SQLServerCPUPercent >= 0
    GROUP BY ServerName;
END

GO

-- [TRACS].[usp_CollectTRACSMetrics]

CREATE   PROCEDURE TRACS.usp_CollectTRACSMetrics
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @ServerName NVARCHAR(128);
    DECLARE @SQL NVARCHAR(MAX);
    
    DECLARE server_cursor CURSOR FAST_FORWARD FOR
    SELECT ServerName FROM TRACS.MonitoredServers WHERE IsActive = 1;
    
    OPEN server_cursor;
    FETCH NEXT FROM server_cursor INTO @ServerName;
    
    WHILE @@FETCH_STATUS = 0
    BEGIN
        BEGIN TRY
            SET @SQL = N'
            INSERT INTO DBAOpsRepository.TRACS.PerformanceSnapshots (
                ServerName, SQLServerCPUPercent, SystemCPUPercent, SystemIdlePercent,
                TotalMemoryMB, AvailableMemoryMB, MemoryUsedPercent,
                PageLifeExpectancy, ActiveSessions, BlockedSessions, BatchRequestsPerSec
            )
            SELECT 
                ''' + @ServerName + N''' AS ServerName,
                x.SQLProcessUtilization,
                x.SystemUtilization,
                x.SystemIdle,
                m.total_physical_memory_kb / 1024,
                m.available_physical_memory_kb / 1024,
                CAST((1.0 - (CAST(m.available_physical_memory_kb AS FLOAT) / NULLIF(m.total_physical_memory_kb, 0))) * 100 AS DECIMAL(5,2)),
                p.cntr_value,
                s.active_sessions,
                s.blocked_sessions,
                b.cntr_value
            FROM [' + @ServerName + N'].master.sys.dm_os_sys_memory m
            CROSS APPLY (
                SELECT TOP 1
                    record.value(''(./Record/SchedulerMonitorEvent/SystemHealth/ProcessUtilization)[1]'', ''int'') AS SQLProcessUtilization,
                    100 - record.value(''(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]'', ''int'') AS SystemUtilization,
                    record.value(''(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]'', ''int'') AS SystemIdle
                FROM (
                    SELECT TOP 1 CONVERT(xml, record) AS record 
                    FROM [' + @ServerName + N'].master.sys.dm_os_ring_buffers 
                    WHERE ring_buffer_type = N''RING_BUFFER_SCHEDULER_MONITOR'' 
                    AND record LIKE ''%<SystemHealth>%'' 
                    ORDER BY [timestamp] DESC
                ) AS rb
            ) x
            CROSS APPLY (
                SELECT cntr_value 
                FROM [' + @ServerName + N'].master.sys.dm_os_performance_counters 
                WHERE counter_name = ''Page life expectancy'' 
                AND object_name LIKE ''%Buffer Manager%''
            ) p
            CROSS APPLY (
                SELECT 
                    COUNT(*) AS active_sessions, 
                    SUM(CASE WHEN blocked <> 0 THEN 1 ELSE 0 END) AS blocked_sessions 
                FROM [' + @ServerName + N'].master.sys.sysprocesses 
                WHERE spid > 50
            ) s
            CROSS APPLY (
                SELECT cntr_value 
                FROM [' + @ServerName + N'].master.sys.dm_os_performance_counters 
                WHERE counter_name = ''Batch Requests/sec'' 
                AND object_name LIKE ''%SQL Statistics%''
            ) b;';
            
            EXEC sp_executesql @SQL;
            
        END TRY
        BEGIN CATCH
            -- Log error with -1 indicator
            INSERT INTO TRACS.PerformanceSnapshots (ServerName, SQLServerCPUPercent)
            VALUES (@ServerName, -1);
            
            PRINT 'Error on ' + @ServerName + ': ' + ERROR_MESSAGE();
        END CATCH
        
        FETCH NEXT FROM server_cursor INTO @ServerName;
    END
    
    CLOSE server_cursor;
    DEALLOCATE server_cursor;
    
    -- Cleanup old data (90 days retention)
    DELETE FROM TRACS.PerformanceSnapshots WHERE CaptureTime < DATEADD(DAY, -90, GETDATE());
END

GO

-- [TRACS].[usp_DailyHealthReport]

CREATE PROCEDURE TRACS.usp_DailyHealthReport
    @OnlyOnIssues BIT = 1  -- NEW: Only send if issues detected
AS
BEGIN
    SET NOCOUNT ON;
    
    DECLARE @ReportDate DATE = CAST(GETDATE() - 1 AS DATE);
    DECLARE @Subject NVARCHAR(255);
    DECLARE @Body NVARCHAR(MAX);
    DECLARE @TotalServers INT;
    DECLARE @HealthyServers INT;
    DECLARE @MaxCPU DECIMAL(5,2);
    DECLARE @AvgCPU DECIMAL(5,2);
    DECLARE @MaxMemory DECIMAL(5,2);
    DECLARE @TotalBlockingEvents INT;
    DECLARE @MaxCPUServer NVARCHAR(128);
    DECLARE @HasIssues BIT = 0;
    
    -- Get metrics from yesterday
    SELECT 
        @TotalServers = COUNT(DISTINCT ServerName),
        @MaxCPU = ISNULL(MAX(SQLServerCPUPercent), 0),
        @AvgCPU = ISNULL(AVG(SQLServerCPUPercent), 0),
        @MaxMemory = ISNULL(MAX(MemoryUsedPercent), 0),
        @TotalBlockingEvents = ISNULL(SUM(BlockedSessions), 0)
    FROM TRACS.PerformanceSnapshots
    WHERE CAST(CaptureTime AS DATE) = @ReportDate
    AND SQLServerCPUPercent >= 0;
    
    -- Determine if there are issues
    IF @MaxCPU > 80 OR @MaxMemory > 90 OR @TotalBlockingEvents > 10
        SET @HasIssues = 1;
    
    -- =====================================================
    -- SMART EMAIL LOGIC: Only send if issues detected
    -- =====================================================
    IF @OnlyOnIssues = 1 AND @HasIssues = 0
    BEGIN
        PRINT 'DBAOps | JB: TRACS healthy - ' + CAST(ISNULL(@TotalServers, 0) AS VARCHAR) + 
            ' servers, Avg CPU ' + CAST(CAST(ISNULL(@AvgCPU, 0) AS INT) AS VARCHAR) + '%. Email not sent.';
        RETURN;
    END;
    
    -- Get server with highest CPU
    SELECT TOP 1 @MaxCPUServer = ServerName
    FROM TRACS.PerformanceSnapshots
    WHERE CAST(CaptureTime AS DATE) = @ReportDate
    AND SQLServerCPUPercent = @MaxCPU
    AND SQLServerCPUPercent >= 0;
    
    -- Count healthy servers
    SELECT @HealthyServers = COUNT(DISTINCT ServerName)
    FROM TRACS.PerformanceSnapshots
    WHERE CAST(CaptureTime AS DATE) = @ReportDate
    AND SQLServerCPUPercent < 80
    AND SQLServerCPUPercent >= 0;
    
    SET @Subject = N'TRACS Health Alert - ' + FORMAT(@ReportDate, 'MMM dd, yyyy') +
        CASE WHEN @MaxCPU > 90 THEN ' - HIGH CPU' WHEN @TotalBlockingEvents > 10 THEN ' - BLOCKING' ELSE '' END;
    
    SET @Body = N'TRACS Health Alert (' + FORMAT(@ReportDate, 'MMM dd') + N'): ' +
        CAST(ISNULL(@HealthyServers, 0) AS NVARCHAR) + N'/' + CAST(ISNULL(@TotalServers, 0) AS NVARCHAR) + N' servers healthy, ' +
        N'Avg CPU ' + CAST(CAST(ISNULL(@AvgCPU, 0) AS INT) AS NVARCHAR) + N'%, ' +
        N'Max CPU ' + CAST(CAST(ISNULL(@MaxCPU, 0) AS INT) AS NVARCHAR) + N'% (' + ISNULL(@MaxCPUServer, 'N/A') + N'), ' +
        N'Max Memory ' + CAST(CAST(ISNULL(@MaxMemory, 0) AS INT) AS NVARCHAR) + N'%, ' +
        CAST(ISNULL(@TotalBlockingEvents, 0) AS NVARCHAR) + N' blocking events.' +
        CHAR(13) + CHAR(10) + CHAR(13) + CHAR(10) +
        N'This report only sent when issues detected.';
    
    EXEC msdb.dbo.sp_send_dbmail
        @profile_name = 'DBAOpsProfile',
        @recipients = N'JLorenz@YOURORG.GOV;VGOODSON@YOURORG.GOV;TVaught@YOURORG.GOV;SQLServerAdminNotification@YOURORG.gov',
        @subject = @Subject,
        @body = @Body,
        @body_format = 'TEXT';
    
    PRINT 'DBAOps | JB: TRACS health alert sent';
    
    SELECT @Subject AS Subject, @Body AS Report;
END;

GO

