<?php
$message = '';
$data = array();

// Handle query task for bootstrap-table AJAX requests
if (!empty($_REQUEST['task'])) {
  $task = $_REQUEST['task'];
  
  if ($task == 'query') {
    $data = queryRequest();
    ajaxResponse($data);
    return;
  }
}

// Handle legacy action-based requests
if ( canEdit('Monitors') ) {
  switch ( $_REQUEST['action'] ) {
  case 'sort' :
  {
    $monitor_ids = $_POST['monitor_ids'];
    # Two concurrent sorts could generate odd sorting... so lock the table.
    global $dbConn;
    $dbConn->beginTransaction();
    $dbConn->exec('LOCK TABLES Monitors WRITE');
    for ( $i = 0; $i < count($monitor_ids); $i += 1 ) {
      $monitor_id = $monitor_ids[$i];
      $monitor_id = preg_replace('/^monitor_id-/', '', $monitor_id);
      if ( ( !$monitor_id ) or ! ( is_integer($monitor_id) or ctype_digit($monitor_id) ) ) {
        Warning('Got '.$monitor_id.' from '.$monitor_ids[$i]);
        continue;
      }
      dbQuery('UPDATE Monitors SET Sequence=? WHERE Id=?', array($i, $monitor_id));
    } // end for each monitor_id
    $dbConn->commit();
    $dbConn->exec('UNLOCK TABLES');

    return;
  } // end case sort
  default:
    ZM\Warning('unknown action '.$_REQUEST['action']);
  }
} else {
  ZM\Warning('Cannot edit monitors');
}

ajaxError('Unrecognised action '.$_REQUEST['action'].' or insufficient permissions for user ' . $user->Username());

//
// FUNCTION DEFINITIONS
//

function queryRequest() {
  global $user, $Servers;
  require_once('includes/Monitor.php');
  require_once('includes/Group_Monitor.php');
  
  $data = array(
    'total' => 0,
    'totalNotFiltered' => 0,
    'rows' => array()
  );
  
  // Get pagination parameters
  $offset = 0;
  if (isset($_REQUEST['offset']) and ($_REQUEST['offset'] != 'NaN')) {
    if ((!is_int($_REQUEST['offset']) and !ctype_digit($_REQUEST['offset']))) {
      ZM\Error('Invalid value for offset: ' . $_REQUEST['offset']);
    } else {
      $offset = $_REQUEST['offset'];
    }
  }
  
  $limit = 0;
  if (isset($_REQUEST['limit']) and ($_REQUEST['limit'] != 'NaN')) {
    if ((!is_int($_REQUEST['limit']) and !ctype_digit($_REQUEST['limit']))) {
      ZM\Error('Invalid value for limit: ' . $_REQUEST['limit']);
    } else {
      $limit = $_REQUEST['limit'];
    }
  }
  
  // Get search parameter
  $search = isset($_REQUEST['search']) ? $_REQUEST['search'] : '';
  
  // Get sort parameters
  $sort = isset($_REQUEST['sort']) ? $_REQUEST['sort'] : 'Sequence';
  $order = isset($_REQUEST['order']) ? strtoupper($_REQUEST['order']) : 'ASC';
  
  // Build monitor query with filters from request parameters (stateless)
  $conditions = array();
  $values = array();
  
  // Get filter values directly from request
  $request_filters = array(
    'GroupId' => isset($_REQUEST['GroupId']) ? $_REQUEST['GroupId'] : null,
    'ServerId' => isset($_REQUEST['ServerId']) ? $_REQUEST['ServerId'] : null,
    'StorageId' => isset($_REQUEST['StorageId']) ? $_REQUEST['StorageId'] : null,
    'Capturing' => isset($_REQUEST['Capturing']) ? $_REQUEST['Capturing'] : null,
    'Analysing' => isset($_REQUEST['Analysing']) ? $_REQUEST['Analysing'] : null,
    'Recording' => isset($_REQUEST['Recording']) ? $_REQUEST['Recording'] : null,
    'Status' => isset($_REQUEST['Status']) ? $_REQUEST['Status'] : null,
    'MonitorId' => isset($_REQUEST['MonitorId']) ? $_REQUEST['MonitorId'] : null,
    'MonitorName' => isset($_REQUEST['MonitorName']) ? $_REQUEST['MonitorName'] : null,
    'Source' => isset($_REQUEST['Source']) ? $_REQUEST['Source'] : null
  );
  
  // Apply request filters to SQL
  if ($request_filters['GroupId']) {
    $GroupIds = is_array($request_filters['GroupId']) ? $request_filters['GroupId'] : array($request_filters['GroupId']);
    $conditions[] = 'M.Id IN (SELECT MonitorId FROM Groups_Monitors WHERE GroupId IN (' . implode(',', array_fill(0, count($GroupIds), '?')) . '))';
    $values = array_merge($values, $GroupIds);
  }
  
  foreach (array('ServerId','StorageId') as $filter) {
    if ($request_filters[$filter]) {
      $filter_values = is_array($request_filters[$filter]) ? $request_filters[$filter] : array($request_filters[$filter]);
      if (count($filter_values)) {
        $conditions[] = 'M.'.$filter.' IN (' . implode(',', array_fill(0, count($filter_values), '?')) . ')';
        $values = array_merge($values, $filter_values);
      }
    }
  }
  
  foreach (array('Capturing','Analysing','Recording') as $filter) {
    if ($request_filters[$filter]) {
      $filter_values = is_array($request_filters[$filter]) ? $request_filters[$filter] : array($request_filters[$filter]);
      if (count($filter_values)) {
        $conditions[] = 'M.'.$filter.' IN (' . implode(',', array_fill(0, count($filter_values), '?')) . ')';
        $values = array_merge($values, $filter_values);
      }
    }
  }
  
  if ($request_filters['Status']) {
    $status_values = is_array($request_filters['Status']) ? $request_filters['Status'] : array($request_filters['Status']);
    if (count($status_values)) {
      $conditions[] = 'COALESCE(S.Status, IF(M.Type="WebSite","Running","NotRunning")) IN (' . implode(',', array_fill(0, count($status_values), '?')) . ')';
      $values = array_merge($values, $status_values);
    }
  }
  
  // Build SQL query
  $sql = 'SELECT M.*, S.*, E.*, (SELECT Name FROM Manufacturers WHERE Manufacturers.Id=M.ManufacturerId) AS Manufacturer, (SELECT Name FROM Models where Models.Id=M.ModelId) AS Model
    FROM Monitors AS M
    LEFT JOIN Monitor_Status AS S ON S.MonitorId=M.Id 
    LEFT JOIN Event_Summaries AS E ON E.MonitorId=M.Id 
    WHERE M.`Deleted`=false';
  
  if (count($conditions)) {
    $sql .= ' AND ' . implode(' AND ', $conditions);
  }
  
  // Get total count before filtering
  $monitors = dbFetchAll($sql, null, $values);
  $unfiltered_monitors = array();
  foreach ($monitors as $monitor) {
    if (visibleMonitor($monitor['Id'])) {
      $unfiltered_monitors[] = $monitor;
    }
  }
  $data['totalNotFiltered'] = count($unfiltered_monitors);
  
  // Apply search filter
  $filtered_monitors = $unfiltered_monitors;
  if ($search != '') {
    $search_lower = strtolower($search);
    $filtered_monitors = array_filter($unfiltered_monitors, function($monitor) use ($search_lower) {
      // Search across common fields without creating Monitor object
      return (
        stripos($monitor['Name'], $search_lower) !== false ||
        stripos($monitor['Function'], $search_lower) !== false ||
        stripos($monitor['Path'], $search_lower) !== false ||
        stripos($monitor['Device'], $search_lower) !== false ||
        stripos($monitor['Host'], $search_lower) !== false ||
        stripos($monitor['Id'], $search_lower) !== false ||
        (isset($monitor['Status']) && stripos($monitor['Status'], $search_lower) !== false)
      );
    });
  }
  
  // Apply MonitorName and Source request filters
  if ($request_filters['MonitorName']) {
    $regexp = $request_filters['MonitorName'];
    if (!strpos($regexp, '/')) $regexp = '/'.$regexp.'/i';
    $filtered_monitors = array_filter($filtered_monitors, function($monitor) use ($regexp) {
      return @preg_match($regexp, $monitor['Name']);
    });
  }
  
  if ($request_filters['Source']) {
    $regexp = $request_filters['Source'];
    if (!preg_match("/^\/.+\/[a-z]*$/i", $regexp))
      $regexp = '/'.$regexp.'/i';
    $filtered_monitors = array_filter($filtered_monitors, function($monitor) use ($regexp) {
      // Match against Path field directly instead of creating Monitor object
      return (preg_match($regexp, $monitor['Path']) || preg_match($regexp, $monitor['Device']) || preg_match($regexp, $monitor['Host']));
    });
  }
  
  // Apply MonitorId filter
  if ($request_filters['MonitorId']) {
    $monitor_ids = is_array($request_filters['MonitorId']) ? $request_filters['MonitorId'] : array($request_filters['MonitorId']);
    $filtered_monitors = array_filter($filtered_monitors, function($monitor) use ($monitor_ids) {
      return in_array($monitor['Id'], $monitor_ids);
    });
  }
  
  $data['total'] = count($filtered_monitors);
  
  // Sort monitors
  usort($filtered_monitors, function($a, $b) use ($sort, $order) {
    $aVal = isset($a[$sort]) ? $a[$sort] : '';
    $bVal = isset($b[$sort]) ? $b[$sort] : '';
    
    if (is_numeric($aVal) && is_numeric($bVal)) {
      $result = $aVal - $bVal;
    } else {
      $result = strcasecmp($aVal, $bVal);
    }
    
    return $order == 'ASC' ? $result : -$result;
  });
  
  // Apply pagination
  if ($limit > 0) {
    $filtered_monitors = array_slice($filtered_monitors, $offset, $limit);
  } else {
    $filtered_monitors = array_slice($filtered_monitors, $offset);
  }
  
  // Get storage areas and servers
  $storage_areas = ZM\Storage::find();
  $StorageById = array();
  foreach ($storage_areas as $S) {
    $StorageById[$S->Id()] = $S;
  }
  
  $ServersById = array();
  foreach ($Servers as $s) {
    $ServersById[$s->Id()] = $s;
  }
  
  // Get group IDs for each monitor
  $monitor_ids = array_map(function($m) { return $m['Id']; }, $filtered_monitors);
  $group_ids_by_monitor_id = array();
  if (count($monitor_ids)) {
    foreach (ZM\Group_Monitor::find(array('MonitorId'=>$monitor_ids)) as $GM) {
      if (!isset($group_ids_by_monitor_id[$GM->MonitorId()]))
        $group_ids_by_monitor_id[$GM->MonitorId()] = array();
      $group_ids_by_monitor_id[$GM->MonitorId()][] = $GM->GroupId();
    }
  }
  
  // Process each monitor and build row data
  $footer_totals = array(
    'monitor_count' => count($filtered_monitors),
    'total_bandwidth' => 0,
    'total_fps' => 0,
    'total_analysis_fps' => 0,
    'total_zones' => 0,
    'event_totals' => array(
      'Total' => array('events' => 0, 'diskspace' => 0),
      'Hour' => array('events' => 0, 'diskspace' => 0),
      'Day' => array('events' => 0, 'diskspace' => 0),
      'Week' => array('events' => 0, 'diskspace' => 0),
      'Month' => array('events' => 0, 'diskspace' => 0),
      'Archived' => array('events' => 0, 'diskspace' => 0)
    )
  );
  
  foreach ($filtered_monitors as $monitor) {
    $Monitor = new ZM\Monitor($monitor);
    $Monitor->GroupIds(isset($group_ids_by_monitor_id[$Monitor->Id()]) ? $group_ids_by_monitor_id[$Monitor->Id()] : array());
    
    // Accumulate footer totals
    $footer_totals['total_bandwidth'] += isset($monitor['CaptureBandwidth']) ? $monitor['CaptureBandwidth'] : 0;
    $footer_totals['total_fps'] += isset($monitor['CaptureFPS']) ? floatval($monitor['CaptureFPS']) : 0;
    $footer_totals['total_analysis_fps'] += isset($monitor['AnalysisFPS']) ? floatval($monitor['AnalysisFPS']) : 0;
    $footer_totals['total_zones'] += isset($monitor['ZoneCount']) ? intval($monitor['ZoneCount']) : 0;
    
    foreach (array('Total', 'Hour', 'Day', 'Week', 'Month', 'Archived') as $period) {
      $footer_totals['event_totals'][$period]['events'] += isset($monitor[$period.'Events']) ? intval($monitor[$period.'Events']) : 0;
      $footer_totals['event_totals'][$period]['diskspace'] += isset($monitor[$period.'EventDiskSpace']) ? intval($monitor[$period.'EventDiskSpace']) : 0;
    }
    
    $row = array();
    $row['Id'] = $monitor['Id'];
    $row['Name'] = validHtmlStr($monitor['Name']);
    $row['Function'] = $monitor['Function'];
    $row['Enabled'] = $monitor['Enabled'];
    $row['Manufacturer'] = $monitor['Manufacturer'];
    $row['Model'] = $monitor['Model'];
    $row['Sequence'] = isset($monitor['Sequence']) ? $monitor['Sequence'] : 0;
    
    // Status
    if (!$monitor['Status']) {
      if ($monitor['Type'] == 'WebSite')
        $monitor['Status'] = 'Running';
      else
        $monitor['Status'] = 'NotRunning';
    }
    $row['Status'] = $monitor['Status'];
    
    // Server
    if (count($Servers)) {
      $Server = isset($ServersById[$monitor['ServerId']]) ? $ServersById[$monitor['ServerId']] : new ZM\Server($monitor['ServerId']);
      $row['Server'] = validHtmlStr($Server->Name());
      $row['ServerId'] = $monitor['ServerId'];
    }
    
    // Source
    $row['Source'] = validHtmlStr($Monitor->Source());
    $row['Width'] = $Monitor->Width();
    $row['Height'] = $Monitor->Height();
    
    // Storage
    if (isset($StorageById[$monitor['StorageId']])) {
      $row['Storage'] = validHtmlStr($StorageById[$monitor['StorageId']]->Name());
    } else if ($monitor['StorageId']) {
      $row['Storage'] = '<span class="error">Deleted '.$monitor['StorageId'].'</span>';
    } else {
      $row['Storage'] = '';
    }
    
    // Event counts with filter querystrings
    $eventCounts = array(
      'Total' => array(
        'filter' => array('Query' => array('terms' => array()))
      ),
      'Hour' => array(
        'filter' => array('Query' => array('terms' => array(
          array('cnj' => 'and', 'attr' => 'StartDateTime', 'op' => '>=', 'val' => '-1 hour')
        )))
      ),
      'Day' => array(
        'filter' => array('Query' => array('terms' => array(
          array('cnj' => 'and', 'attr' => 'StartDateTime', 'op' => '>=', 'val' => '-1 day')
        )))
      ),
      'Week' => array(
        'filter' => array('Query' => array('terms' => array(
          array('cnj' => 'and', 'attr' => 'StartDateTime', 'op' => '>=', 'val' => '-7 day')
        )))
      ),
      'Month' => array(
        'filter' => array('Query' => array('terms' => array(
          array('cnj' => 'and', 'attr' => 'StartDateTime', 'op' => '>=', 'val' => '-1 month')
        )))
      ),
      'Archived' => array(
        'filter' => array('Query' => array('terms' => array(
          array('cnj' => 'and', 'attr' => 'Archived', 'op' => '=', 'val' => '1')
        )))
      )
    );
    
    foreach ($eventCounts as $period => $eventCount) {
      $row[$period.'Events'] = (int)$monitor[$period.'Events'];
      $row[$period.'EventDiskSpace'] = human_filesize($monitor[$period.'EventDiskSpace']);
      
      // Generate filter querystring for this period and monitor
      $filter = addFilterTerm(
        $eventCount['filter'],
        count($eventCount['filter']['Query']['terms']),
        array('cnj' => 'and', 'attr' => 'Monitor', 'op' => '=', 'val' => $monitor['Id'])
      );
      parseFilter($filter);
      $row[$period.'FilterQuery'] = $filter['querystring'];
    }
    
    // Zone count
    $row['ZoneCount'] = $monitor['ZoneCount'];
    
    // FPS and bandwidth
    $row['CaptureFPS'] = isset($monitor['CaptureFPS']) ? $monitor['CaptureFPS'] : '0.00';
    $row['AnalysisFPS'] = isset($monitor['AnalysisFPS']) ? $monitor['AnalysisFPS'] : '0.00';

    // Format bandwidth with units (bytes per second) - use human_filesize for consistency
    $bandwidth = isset($monitor['CaptureBandwidth']) ? $monitor['CaptureBandwidth'] : 0;
    if ($bandwidth > 0) {
      $row['CaptureBandwidth'] = human_filesize($bandwidth).'/s';
    } else {
      $row['CaptureBandwidth'] = '';
    }
    $row['Analysing'] = isset($monitor['Analysing']) ? $monitor['Analysing'] : 'None';
    $row['Recording'] = isset($monitor['Recording']) ? $monitor['Recording'] : 'None';
    // console.js treats this as both an enable flag AND the text to display:
    //   if (row.ONVIF_Event_Listener) html += "Use ONVIF '" + row.ONVIF_Event_Listener + "'"
    // So send the alarm text only when the listener is actually enabled, else 0.
    $row['ONVIF_Event_Listener'] = !empty($monitor['ONVIF_Event_Listener']) ? $monitor['ONVIF_Alarm_Text'] : 0;
    $row['UpdatedOn'] = isset($monitor['UpdatedOn']) ? $monitor['UpdatedOn'] : '';
    $row['Type'] = $monitor['Type'];
    $row['Capturing'] = isset($monitor['Capturing']) ? $monitor['Capturing'] : 'None';
    
    // Groups
    if (canView('Groups')) {
      $groups_html = implode('<br/>',
        array_map(function($group_id) {
          $Group = ZM\Group::find_one(array('Id'=>$group_id));
          if ($Group) {
            $Groups = $Group->Parents();
            array_push($Groups, $Group);
          } else {
            $Groups = array();
          }
          return implode(' &gt; ', array_map(function($Group) {
            if (canView('Stream')) {
              return '<a href="?view=montagereview&amp;GroupId='.$Group->Id().'">'.validHtmlStr($Group->Name()).'</a>';
            } else {
              return validHtmlStr($Group->Name());
            }
          }, $Groups));
        }, $Monitor->GroupIds())
      );
      $row['Groups'] = $groups_html;
    } else {
      $row['Groups'] = '';
    }
    
    // Thumbnail
    $row['Thumbnail'] = '';
    if (ZM_WEB_LIST_THUMBS && ($monitor['Capturing'] != 'None') && canView('Stream')) {
      $ratio_factor = $Monitor->ViewWidth() ? $Monitor->ViewHeight() / $Monitor->ViewWidth() : 1;
      $options = array(
        'width'  => ZM_WEB_LIST_THUMB_WIDTH,
        'height' => ZM_WEB_LIST_THUMB_HEIGHT ? ZM_WEB_LIST_THUMB_HEIGHT : ZM_WEB_LIST_THUMB_WIDTH * $ratio_factor,
        'scale'  => $Monitor->ViewWidth() ? intval(100 * ZM_WEB_LIST_THUMB_WIDTH / $Monitor->ViewWidth()) : 100,
        'mode'   => 'jpeg',
        'frames' => 1,
      );

      $stillSrc = $Monitor->getStreamSrc($options);
      // Calculate optimal scale for the popup stream based on browser width
      // The JS overlay uses 60% of window.innerWidth (see calculateOverlayDimensions in skin.js)
      $browser_width = 1920; // Default fallback
      if (isset($_COOKIE['zmBrowserSizes'])) {
        $zmBrowserSizes = jsonDecode($_COOKIE['zmBrowserSizes']);
        if (!empty($zmBrowserSizes['innerWidth'])) {
          $browser_width = validInt($zmBrowserSizes['innerWidth']);
        }
      }
      $target_width = $browser_width * 0.6; // Match JS overlay sizing (60% of viewport)
      $options['scale'] = $Monitor->ViewWidth() ? intval(100 * $target_width / $Monitor->ViewWidth()) : 100;
      if ($options['scale'] > 100) $options['scale'] = 100;
      else if ($options['scale'] < 10) $options['scale'] = 10;
      unset($options['frames']);
      $streamSrc = $Monitor->getStreamSrc($options);

      $videoAttr = '';
      $go2rtcAttr = '';
      $liveStreamAttr = '';

      // Debug info for troubleshooting
      $debugInfo = 'Analysing='.$monitor['Analysing'].',Decoding='.$monitor['Decoding'].
        ',Go2RTC='.$Monitor->Go2RTCEnabled().
        ',ZM_GO2RTC_PATH='.(defined('ZM_GO2RTC_PATH') ? ZM_GO2RTC_PATH : 'undefined');
      $debugAttr = ' data-debug="'.htmlspecialchars($debugInfo).'"';

      // STILL THUMBNAIL: When not analysing/decoding, use the most recent event thumbnail
      // (ZM's nph-zms can't produce frames when not decoding)
      if ($monitor['Analysing'] == 'None' && $monitor['Decoding'] == 'None') {
        $debugAttr .= ' data-debug-still="event"';
        $event = ZM\Event::find_one(
          array(
            'MonitorId' => $monitor['Id'],
            'EndDateTime' => 'NOT NULL'
          ),
          array('order' => 'Id DESC')
        );
        if ($event) {
          $stillSrc = $event->getThumbnailSrc(array(), '&amp;');
          // Default stream/video sources from event (may be overridden by live streaming below)
          // Use browser width for optimal scale (same as live stream calculation above)
          $event_scale = $event->Width() ? intval(100 * $target_width / $event->Width()) : 100;
          if ($event_scale > 100) $event_scale = 100;
          else if ($event_scale < 10) $event_scale = 10;
          $streamSrc = $event->getStreamSrc(array(
            'mode' => 'jpeg', 'scale' => $event_scale, 'maxfps' => ZM_WEB_VIDEO_MAXFPS,
            'replay' => 'single', 'rate' => '400'), '&amp;');
          if ($event->DefaultVideo()) {
            $videoSrc = $event->getStreamSrc(array('mode' => 'mp4'), '&amp;');
            $videoAttr = ' video_src="'.$videoSrc.'" data-event-start="'.htmlspecialchars($event->StartDateTime()).'"';
          }
        }
      } else {
        $debugAttr .= ' data-debug-still="monitor"';
      }

      // HOVER OVERLAY: Check for external streaming methods (these work independently of ZM's decode/analyze)
      // Priority: go2rtc > rtsp2web > janus > event replay > ZM native stream
      if ($Monitor->Go2RTCEnabled() && defined('ZM_GO2RTC_PATH') && ZM_GO2RTC_PATH) {
        $liveStreamAttr = ' data-stream-type="go2rtc"'.
          ' data-go2rtc-src="'.htmlspecialchars(ZM_GO2RTC_PATH).'"'.
          ' data-monitor-id="'.$monitor['Id'].'"';
        $go2rtcAttr = ' go2rtc_src="'.htmlspecialchars(ZM_GO2RTC_PATH).'" go2rtc_mid="'.$monitor['Id'].'"';
        $debugAttr .= ' data-debug-overlay="go2rtc"';
      } else if ($Monitor->RTSP2WebEnabled() && defined('ZM_RTSP2WEB_PATH') && ZM_RTSP2WEB_PATH) {
        $liveStreamAttr = ' data-stream-type="rtsp2web"'.
          ' data-rtsp2web-src="'.htmlspecialchars(ZM_RTSP2WEB_PATH).'"'.
          ' data-stream-channel="'.($Monitor->StreamChannel() ?: 'Restream').'"'.
          ' data-monitor-id="'.$monitor['Id'].'"';
        $debugAttr .= ' data-debug-overlay="rtsp2web"';
      } else if ($Monitor->JanusEnabled()) {
        $janusPath = defined('ZM_JANUS_PATH') && ZM_JANUS_PATH ? ZM_JANUS_PATH : '';
        $liveStreamAttr = ' data-stream-type="janus"'.
          ' data-janus-src="'.htmlspecialchars($janusPath).'"'.
          ' data-monitor-id="'.$monitor['Id'].'"';
        $debugAttr .= ' data-debug-overlay="janus"';
      } else if ($monitor['Analysing'] == 'None' && $monitor['Decoding'] == 'None') {
        // No external streaming and ZM not running - overlay will use event replay (already set above)
        $debugAttr .= ' data-debug-overlay="event-replay"';
      } else {
        // Use native ZM streaming (zms) - Analysing or Decoding is running
        $debugAttr .= ' data-debug-overlay="zm-native"';
      }

      $thmbWidth = ($options['width']) ? 'width:'.$options['width'].'px;' : '';
      $thmbHeight = ($options['height']) ? 'height:'.$options['height'].'px;' : '';

      $row['Thumbnail'] = '<div class="colThumbnail" style="'.$thmbHeight.'"><a href="?view=watch&amp;mid='.$monitor['Id'].'">'.
        '<img id="thumbnail'.$Monitor->Id().'" src="'.$stillSrc.'" style="'.$thmbWidth.$thmbHeight.
        '" stream_src="'.$streamSrc.'" still_src="'.$stillSrc.'"'.$videoAttr.$go2rtcAttr.$liveStreamAttr.$debugAttr.
        ' data-monitor-width="'.$Monitor->ViewWidth().'" data-monitor-height="'.$Monitor->ViewHeight().'"'.
        ($options['width'] ? ' width="'.$options['width'].'"' : '').
        ($options['height'] ? ' height="'.$options['height'].'"' : '').
        ' loading="lazy" /></a></div>';
    }
    
    $data['rows'][] = $row;
  }
  
  // Add footer totals to response
  $data['footer'] = array(
    'monitor_count' => $footer_totals['monitor_count'],
    'bandwidth_fps' => human_filesize($footer_totals['total_bandwidth']).'/s '.
                       round($footer_totals['total_fps'], 2).' fps / '.
                       round($footer_totals['total_analysis_fps'], 2).' fps',
    'total_zones' => $footer_totals['total_zones']
  );

  // Build filter querystrings for footer event totals (matching header behavior)
  $eventCountsFooter = array(
    'Total' => array(
      'filter' => array('Query' => array('terms' => array()))
    ),
    'Hour' => array(
      'filter' => array('Query' => array('terms' => array(
        array('cnj' => 'and', 'attr' => 'StartDateTime', 'op' => '>=', 'val' => '-1 hour')
      )))
    ),
    'Day' => array(
      'filter' => array('Query' => array('terms' => array(
        array('cnj' => 'and', 'attr' => 'StartDateTime', 'op' => '>=', 'val' => '-1 day')
      )))
    ),
    'Week' => array(
      'filter' => array('Query' => array('terms' => array(
        array('cnj' => 'and', 'attr' => 'StartDateTime', 'op' => '>=', 'val' => '-7 day')
      )))
    ),
    'Month' => array(
      'filter' => array('Query' => array('terms' => array(
        array('cnj' => 'and', 'attr' => 'StartDateTime', 'op' => '>=', 'val' => '-1 month')
      )))
    ),
    'Archived' => array(
      'filter' => array('Query' => array('terms' => array(
        array('cnj' => 'and', 'attr' => 'Archived', 'op' => '=', 'val' => '1')
      )))
    )
  );

  // Add formatted event totals to footer with filter querystrings
  foreach (array('Total', 'Hour', 'Day', 'Week', 'Month', 'Archived') as $period) {
    $data['footer'][$period.'Events'] = $footer_totals['event_totals'][$period]['events'];
    $data['footer'][$period.'EventDiskSpace'] = human_filesize($footer_totals['event_totals'][$period]['diskspace']);

    // Generate filter querystring for footer (include monitor filter if monitors are filtered)
    $filter = $eventCountsFooter[$period]['filter'];
    if (count($monitor_ids) > 0) {
      $filter = addFilterTerm(
        $filter,
        count($filter['Query']['terms']),
        array('cnj' => 'and', 'attr' => 'Monitor', 'op' => 'IN', 'val' => implode(',', $monitor_ids))
      );
    }
    parseFilter($filter);
    $data['footer'][$period.'FilterQuery'] = $filter['querystring'];
  }
  
  return $data;
}
?>
