#!/usr/bin/env python3
"""
Dead Miner Hunter - Clean Python Script
File: /var/www/html/ngon/status/dead.py

This script identifies miners with errors and presents them on a dynamic HTML page.
"""

import pandas as pd
import json
import os
import re
import sys
from html import escape

# Import the new config manager
sys.path.insert(0, '/opt/ngon/apps')
from managers.config_manager import config

# InfluxDB imports
import influxdb_client
from influxdb_client import InfluxDBClient
from datetime import datetime, timezone

class DeadMinerHunter:
    """Handles loading and processing of miner and error data."""
    def __init__(self):
        self.miners_data = []
        self.all_miners_data = []  # Store ALL miners for statistics
        self.error_codes = {}
        self.sleeping_miners = set()
        self.inventory_data = {}
        self.zero_hash_no_errors = []  # Track zero hash miners without errors
        self.pod_miner_types = {}  # Track pod to miner type mapping
        self.reboot_counts = {}  # Track reboot counts from InfluxDB
        
        # Initialize InfluxDB client
        self.init_influxdb()
        self.load_data()
        self.load_reboot_data()

    def init_influxdb(self):
        """Initialize InfluxDB client for reboot tracking."""
        try:
            # InfluxDB settings (from /opt/ngon/apps/miners/miner_monitor.py)
            token = 'rehPqmCSlCkQnEAaFQEX75JD7J9lAsflaIS6TTYnIh3SetVyzEumX_dRMizxFEsEISG4VUdTd70rUHcJig7V6w=='
            org = "ngonsolutions"
            url = "https://us-central1-1.gcp.cloud2.influxdata.com"
            self.influx_bucket = 'MARAGON_Miner_Status'
            
            self.influx_client = InfluxDBClient(url=url, token=token, org=org)
            self.influx_query_api = self.influx_client.query_api()
        except Exception as e:
            # Fail silently if InfluxDB is not available
            self.influx_client = None
            self.influx_query_api = None

    def load_reboot_data(self):
        """Load reboot counts from InfluxDB by counting reboot events."""
        if not self.influx_query_api:
            return
        
        try:
            # Query for all reboot events in the last 30 days and count them per MAC
            query = f'''
            from(bucket: "{self.influx_bucket}")
                |> range(start: -30d)
                |> filter(fn: (r) => r._measurement == "miner_reboots")
                |> filter(fn: (r) => r._field == "count")
                |> group(columns: ["mac"])
                |> sum()
            '''
            
            result = self.influx_query_api.query(query)
            
            # Process results
            for table in result:
                for record in table.records:
                    mac = record.values.get('mac', '').upper()
                    reboot_count = int(record.values.get('_value', 0))
                    if mac and reboot_count > 0:
                        self.reboot_counts[mac] = reboot_count
                        
        except Exception as e:
            # Fail silently if query fails
            pass

    def get_reboot_count(self, mac):
        """Get reboot count for a specific MAC address."""
        if not mac:
            return 0
        return self.reboot_counts.get(mac.upper(), 0)

    def load_data(self):
        """Loads data from various CSV files into memory."""
        try:
            # File paths
            base_path = '/opt/ngon/data/live/'
            miners_file = os.path.join(base_path, 'miner_status.csv')
            inventory_file = os.path.join(base_path, 'miner_inventory.csv')
            error_codes_file = '/opt/ngon/config/error_codes.csv'
            # Load miner types from config manager - use available method
            try:
                self.pod_miner_types = config.get_pod_miner_counts() if hasattr(config, 'get_pod_miner_counts') else {}
            except:
                self.pod_miner_types = {}

            # Load inventory data
            if os.path.exists(inventory_file):
                df_inv = pd.read_csv(inventory_file)
                self.inventory_data = {
                    str(row['MAC']).strip().upper(): {
                        'position': str(row['Position']).strip(),
                        'serial': str(row['Serial']).strip(),
                        'site': str(row['Site']).strip(),
                        'pod': str(row['Pod']).strip()
                    } for _, row in df_inv.iterrows()
                }

            # Load miners data
            if os.path.exists(miners_file):
                df_miners = pd.read_csv(miners_file)
                all_miners = df_miners.to_dict('records')
                
                # Store all miners for statistics
                self.all_miners_data = all_miners

                for miner in all_miners:
                    # Process errors
                    error_data = miner.get('error_codes', miner.get('errors', ''))
                    if pd.notna(error_data) and str(error_data).strip() and str(error_data) != '0':
                        miner['errors'] = [e.strip() for e in str(error_data).split(',') if e.strip()]
                    else:
                        miner['errors'] = []

                    # Standardize hashrate and add inventory data
                    miner['hashrate'] = miner.get('hs_rt', 0)
                    mac = str(miner.get('mac', '')).strip().upper() if pd.notna(miner.get('mac')) else ''
                    inv_data = self.inventory_data.get(mac, {})
                    miner['position'] = inv_data.get('position', 'Unknown')
                    miner['serial'] = inv_data.get('serial', 'Unknown')
                    miner['site'] = inv_data.get('site', 'Unknown')
                    miner['inv_pod'] = inv_data.get('pod', 'Unknown')

                # Separate miners with errors and zero-hash miners without errors
                self.miners_data = [miner for miner in all_miners if miner['errors']]
                
                # Find zero-hash miners without errors (excluding sleeping)
                self.zero_hash_no_errors = [
                    miner for miner in all_miners 
                    if miner['hashrate'] == 0 
                    and not miner['errors']
                    and miner.get('mac') not in self.sleeping_miners
                ]

            # Load error codes definitions
            if os.path.exists(error_codes_file):
                df_errors = pd.read_csv(error_codes_file)
                self.error_codes = {
                    str(row['Error Code']).strip(): {
                        'meaning': str(row['Meaning']).strip(),
                        'solution': str(row['Corresponding Solutions']).strip()
                    } for _, row in df_errors.iterrows()
                }

            # Load sleeping miners from miner_status.csv based on mining_state column
            if os.path.exists(miners_file):
                df_miners_for_sleep = pd.read_csv(miners_file)
                sleeping_df = df_miners_for_sleep[df_miners_for_sleep['mining_state'].str.lower() == 'sleeping']
                self.sleeping_miners = set(sleeping_df['mac'].str.strip().str.upper().tolist())

            # Pod miner types already loaded from config manager above

        except Exception:
            # Fail silently if files are missing or corrupt
            pass

    def matches_error_pattern(self, miner_error, pattern_error):
        """Checks if an error code matches a pattern (e.g., '72X')."""
        if 'X' in pattern_error:
            pattern = pattern_error.replace('X', r'\d')
            return bool(re.match(f'^{pattern}$', miner_error))
        return miner_error == pattern_error

    def get_error_info(self, error_code):
        """Retrieves the meaning and solution for a given error code."""
        for pattern, info in self.error_codes.items():
            if self.matches_error_pattern(error_code, pattern):
                return info
        return {'meaning': 'Unknown error', 'solution': 'Contact support'}

    def get_active_miners(self):
        """Returns a list of miners that are not in the sleeping set."""
        return [miner for miner in self.miners_data if miner.get('mac') not in self.sleeping_miners]
    
    def get_zero_hash_with_errors_count(self):
        """Count active miners with zero hash that have errors."""
        active = self.get_active_miners()
        return len([m for m in active if m['hashrate'] == 0])
    
    def get_zero_hash_no_errors_count(self):
        """Count miners with zero hash but no error codes."""
        return len(self.zero_hash_no_errors)

    def group_by_pod(self, miners):
        """Groups a list of miners by their pod name."""
        pods = {}
        for miner in miners:
            pod_name = miner.get('pod', 'Unknown')
            if pod_name not in pods:
                pods[pod_name] = []
            pods[pod_name].append(miner)
        return pods

def generate_html_content(hunter, active_miners, pods, error_info):
    """Generates the dynamic HTML sections for the page."""

    # Generate HTML for each pod section and its miner cards
    pods_html = []
    if not pods:
        pods_html.append('<div class="no-miners">No miners with error codes found</div>')
    else:
        for pod_name in sorted(pods.keys()):
            miners = pods[pod_name]
            miner_cards_html = []
            for miner in miners:
                errors_json = escape(json.dumps(miner.get("errors", [])), quote=True)

                mac = escape(str(miner.get("mac", "Unknown")))
                ip = escape(str(miner.get("ip", "No IP")))
                position = escape(str(miner.get("position", "Unknown")))
                serial = escape(str(miner.get("serial", "Unknown")))
                site = escape(str(miner.get("site", "Unknown")))
                
                # Get reboot count for this miner
                raw_mac = str(miner.get("mac", ""))
                reboot_count = hunter.get_reboot_count(raw_mac)
                
                # Use a variable for hashrate that is not escaped for the data attribute
                hashrate_val = miner.get("hashrate", 0)
                hashrate_str = escape(str(hashrate_val))
                last5_serial = serial[-5:] if len(serial) >= 5 else serial

                error_tags_html = []
                for error in miner.get('errors', []):
                    info = hunter.get_error_info(error)
                    tooltip = escape(f"{info['meaning']} - {info['solution']}", quote=True)
                    error_code_safe = escape(error)
                    error_tags_html.append(f'<span class="error-tag" title="{tooltip}">{error_code_safe}</span>')
                
                # Get miner type for this pod
                pod_name = miner.get('pod', 'Unknown')
                miner_type = hunter.pod_miner_types.get(pod_name, 'Unknown')
                
                # Add data attributes to the miner card
                miner_cards_html.append(f"""
                        <div class="miner-card" data-errors='{errors_json}' data-hashrate="{hashrate_val}" data-position="{position}" data-serial="{last5_serial}" data-site="{site}" data-miner-type="{miner_type}" data-pod="{pod_name}">
                            <div class="miner-info">
                                <div class="miner-details">
                                    <div class="miner-mac">{mac}</div>
                                    <div class="miner-ip">{ip} | {position}{' | Reboots: ' + str(reboot_count) if reboot_count > 0 else ''}</div>
                                </div>
                                <div class="hashrate">{hashrate_str} TH/s</div>
                            </div>
                            <div class="error-codes">
                                <div class="error-label">Error Codes:</div>
                                <div class="error-tags">{''.join(error_tags_html)}</div>
                            </div>
                        </div>
                """)

            pod_name_safe = escape(pod_name, quote=True)
            pod_miner_type = hunter.pod_miner_types.get(pod_name, 'Unknown')
            pods_html.append(f"""
                <div class="pod-section" data-pod="{pod_name_safe}">
                    <div class="pod-header" onclick="togglePod('{pod_name_safe}')">
                        <span>Pod: {escape(pod_name)} ({pod_miner_type})</span>
                        <span>({len(miners)} miners) <span class="pod-toggle">&gt;</span></span>
                    </div>
                    <div class="pod-miners collapsed" data-pod-miners="{pod_name_safe}">
                        {''.join(miner_cards_html)}
                    </div>
                </div>
            """)

    # Generate HTML for the error filter items
    error_filters_html = []
    for error in error_info:
        code = escape(error['code'], quote=True)
        meaning = escape(error['meaning'])
        solution = escape(error['solution'])
        count = error['count']
        error_filters_html.append(f"""
            <div class="error-filter-item selected" data-error="{code}">
                <div class="error-filter-header">
                    <label style="display: flex; align-items: center; cursor: pointer; width: 100%;">
                        <input type="checkbox" class="checkbox" checked>
                        <span class="error-code">{escape(error['code'])}</span>
                    </label>
                    <span class="error-count">{count}</span>
                </div>
                <div class="error-meaning">{meaning}</div>
                <div class="error-solution">{solution}</div>
            </div>
        """)

    return ''.join(pods_html), ''.join(error_filters_html)


def render_page(hunter, active_miners, pods, error_info):
    """Renders the complete HTML page using a template string."""
    pods_html, error_filters_html = generate_html_content(hunter, active_miners, pods, error_info)
    all_error_codes_json = json.dumps([e['code'] for e in error_info])
    # Include all error codes (from CSV + active) so keyword search works against wildcard patterns
    all_error_details = {code: info for code, info in hunter.error_codes.items()}
    for e in error_info:
        if e['code'] not in all_error_details:
            all_error_details[e['code']] = {'meaning': e['meaning'], 'solution': e['solution']}
    error_details_json = json.dumps(all_error_details)
    pod_miner_types_json = json.dumps(hunter.pod_miner_types)
    
    # Get zero hash counts
    zero_hash_with_errors = hunter.get_zero_hash_with_errors_count()
    zero_hash_no_errors = hunter.get_zero_hash_no_errors_count()

    # Import navigation functions
    sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
    from links import generate_header_html, generate_dropdown_css, generate_dropdown_js, generate_dropdown_html
    
    # Generate header HTML manually to include everything on the left
    header_html = f'''
    <div class="header">
        <div>
            <div class="dropdown">
                <h1 class="dropdown-title">NGON Mining - Dead Miner Hunter 🎯</h1>
                <div class="dropdown-content">{generate_dropdown_html()}</div>
            </div>
            <p style="color: #ccc; margin-top: 5px; margin-bottom: 15px;">Identify and manage miners with errors</p>
            <div class="stats-bar" style="display: flex; gap: 20px; flex-wrap: wrap; margin-bottom: 15px;">
                <div class="stat-item">Miners with Errors: {len(active_miners)}</div>
                <div class="stat-item critical">Zero Hash (with errors): {zero_hash_with_errors}</div>
                <div class="stat-item warning">Zero Hash (no errors): {zero_hash_no_errors}</div>
                <div class="stat-item">Sleeping: {len(hunter.sleeping_miners)}</div>
                <div class="stat-item">Error Types: {len(error_info)}</div>
            </div>
            <div style="display: flex; gap: 15px; align-items: center; margin-bottom: 10px;">
                <span style="color: #ccc; font-size: 0.9rem; font-weight: 500;">Hashrate Thresholds:</span>
                <div style="display: flex; align-items: center; gap: 5px;">
                    <label style="color: #ccc; font-size: 0.8rem;">M60:</label>
                    <input type="number" id="m60-threshold" value="170" min="0" style="width: 60px; padding: 4px 8px; border: 1px solid #333; border-radius: 4px; background: #222; color: #fff; font-size: 0.8rem;">
                </div>
                <div style="display: flex; align-items: center; gap: 5px;">
                    <label style="color: #ccc; font-size: 0.8rem;">Jpro:</label>
                    <input type="number" id="jpro-threshold" value="100" min="0" style="width: 60px; padding: 4px 8px; border: 1px solid #333; border-radius: 4px; background: #222; color: #fff; font-size: 0.8rem;">
                </div>
                <div style="display: flex; align-items: center; gap: 5px;">
                    <label style="color: #ccc; font-size: 0.8rem;">XP:</label>
                    <input type="number" id="xp-threshold" value="120" min="0" style="width: 60px; padding: 4px 8px; border: 1px solid #333; border-radius: 4px; background: #222; color: #fff; font-size: 0.8rem;">
                </div>
            </div>
            <div style="display: flex; gap: 15px; align-items: center;">
                <span style="color: #ccc; font-size: 0.9rem; font-weight: 500;">Actions:</span>
                <button onclick="refreshData()" style="padding: 8px 16px; background: rgba(255,255,255,0.05); color: #ffffff; border: 1px solid rgba(255,255,255,0.1); border-radius: 4px; cursor: pointer;">Refresh</button>
            </div>
        </div>
    </div>
    '''

    return f"""<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Dead Miner Hunter</title>
    <style>
        {generate_dropdown_css()}
    </style>
    <style>
        * {{ margin: 0; padding: 0; box-sizing: border-box; }}
        body {{ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #1a1a1a; color: #ffffff; min-height: 100vh; padding: 20px; }}
        .container {{ max-width: none; margin: 0 auto; }}
        .header {{ background-color: rgba(255,255,255,0.05); border-radius: 8px; padding: 30px; margin-bottom: 30px; border: 1px solid rgba(255,255,255,0.1); }}
        h1 {{ font-size: 2.2em; color: #00ff00; margin-bottom: 10px; }}
        .subtitle {{ color: #ccc; font-size: 1.1rem; }}
        .stats-bar {{ display: flex; gap: 20px; margin-top: 20px; flex-wrap: wrap; }}
        .stat-item {{ background-color: rgba(255,255,255,0.05); color: #ffffff; padding: 15px 25px; border-radius: 6px; font-weight: 600; border: 1px solid rgba(255,255,255,0.1); }}
        .stat-item.warning {{ background-color: rgba(255, 102, 0, 0.2); border-color: rgba(255, 102, 0, 0.4); }}
        .stat-item.critical {{ background-color: rgba(255, 0, 0, 0.2); border-color: rgba(255, 0, 0, 0.4); }}
        .main-content {{ display: grid; grid-template-columns: 2fr 1fr 1fr; gap: 15px; height: 800px; }}
        .panel {{ background-color: rgba(255,255,255,0.02); border-radius: 6px; border: 1px solid rgba(255,255,255,0.1); overflow: hidden; height: 100%; display: flex; flex-direction: column; }}
        .panel-header {{ display: flex; justify-content: space-between; align-items: center; background-color: rgba(255,255,255,0.05); color: #ffffff; padding: 15px 20px; font-size: 1.1em; font-weight: bold; border-bottom: 1px solid rgba(255,255,255,0.1); }}
        .miners-content {{ padding: 15px; flex: 1; overflow-y: auto; }}
        .pod-section {{ margin-bottom: 20px; }}
        .pod-header {{ background-color: rgba(255,255,255,0.05); color: #ffffff; padding: 10px 15px; border-radius: 6px; margin-bottom: 8px; font-weight: bold; display: flex; justify-content: space-between; align-items: center; cursor: pointer; transition: background-color 0.3s ease; border: 1px solid rgba(255,255,255,0.1); font-size: 0.85em; }}
        .pod-header:hover {{ background-color: rgba(255,255,255,0.08); }}
        .pod-miners.collapsed {{ max-height: 0; overflow: hidden; }}
        .miner-card {{ background-color: #222; border: 1px solid #333; border-radius: 6px; padding: 15px; margin-bottom: 8px; position: relative; }}
        .miner-card::before {{ content: ''; position: absolute; top: 0; left: 0; height: 3px; width: 100%; background-color: #ff6600; border-radius: 6px 6px 0 0; }}
        .miner-info {{ display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 10px; }}
        .miner-mac {{ font-family: 'Courier New', monospace; font-weight: 600; color: #ffffff; font-size: 0.9rem; }}
        .miner-ip {{ color: #ccc; font-size: 0.85rem; margin-top: 4px; }}
        .hashrate {{ background-color: #ff6600; color: white; padding: 6px 12px; border-radius: 4px; font-weight: 600; font-size: 0.85rem; }}
        .error-codes {{ border-top: 1px solid #333; padding-top: 10px; }}
        .error-label {{ color: #ccc; font-size: 0.75rem; margin-bottom: 6px; font-weight: 500; }}
        .error-tags {{ display: flex; flex-wrap: wrap; gap: 6px; }}
        .error-tag {{ background-color: #ff6600; color: white; padding: 4px 8px; border-radius: 4px; font-size: 0.75rem; font-weight: 500; cursor: help; }}
        .search-box {{ padding: 15px; border-bottom: 1px solid rgba(255,255,255,0.1); }}
        .search-input {{ width: 100%; padding: 10px 15px; border: 1px solid #333; border-radius: 4px; font-size: 0.9rem; background: #222; color: #fff; }}
        .filter-controls {{ padding: 15px; border-bottom: 1px solid rgba(255,255,255,0.1); }}
        .toggle-all-btn {{ width: 100%; padding: 10px; background-color: rgba(255,255,255,0.05); color: #ffffff; border: 1px solid rgba(255,255,255,0.1); border-radius: 4px; cursor: pointer; }}
        .error-filters {{ padding: 15px; flex: 1; overflow-y: auto; }}
        .error-filter-item {{ background-color: #222; border: 1px solid #333; border-radius: 4px; padding: 10px; margin-bottom: 8px; cursor: pointer; }}
        .error-filter-item.selected {{ border-color: #555; background-color: rgba(255,255,255,0.05); }}
        .error-filter-header {{ display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }}
        .error-code {{ font-family: 'Courier New', monospace; font-weight: 600; }}
        .error-count {{ background-color: rgba(255,255,255,0.1); padding: 3px 8px; border-radius: 4px; font-size: 0.75rem; }}
        .error-meaning {{ color: #ccc; font-size: 0.8rem; margin-bottom: 4px; }}
        .error-solution {{ color: #aaa; font-size: 0.75rem; font-style: italic; }}
        .copy-textarea {{ width: 100%; flex: 1; padding: 15px; border: none; font-family: 'Courier New', monospace; background: #222; color: #fff; resize: none; outline: none; }}
        .copy-controls {{ padding: 15px; border-bottom: 1px solid rgba(255,255,255,0.1); display: flex; gap: 10px; align-items: center; }}
        .copy-btn {{ background-color: rgba(255,255,255,0.05); color: #ffffff; border: 1px solid rgba(255,255,255,0.1); padding: 6px 12px; border-radius: 4px; cursor: pointer; }}
        .copy-count {{ color: #ccc; font-size: 0.85rem; }}
        .no-miners {{ text-align: center; padding: 40px 20px; color: #ccc; }}
        .hidden {{ display: none; }}
        .zero-hash-toggle {{ font-size: 0.8rem; font-weight: normal; display: flex; align-items: center; gap: 8px; }}
        .threshold-controls {{ display: flex; gap: 15px; align-items: center; }}
        .threshold-input {{ display: flex; align-items: center; gap: 5px; font-size: 0.8rem; }}
        .threshold-input label {{ color: #ccc; }}
        .threshold-input input {{ width: 60px; padding: 4px 8px; border: 1px solid #333; border-radius: 4px; background: #222; color: #fff; font-size: 0.8rem; }}
        @media (max-width: 1400px) {{ .main-content {{ grid-template-columns: 1fr; }} }}
    </style>
</head>
<body>
<div class="container">
    {header_html}
    <div class="main-content">
        <div class="panel">
            <div class="panel-header">
                <span style="color: #00ff00;">Miners with Errors by Pod</span>
                <div style="display: flex; align-items: center; gap: 15px;">
                    <button class="copy-btn" id="expand-all-btn" onclick="expandAllPods()">Expand All</button>
                    <div class="zero-hash-toggle">
                        <label for="zero-hash-filter">Only Zero Hashrate</label>
                        <input type="checkbox" id="zero-hash-filter">
                    </div>
                    <div class="zero-hash-toggle">
                        <label for="no-zero-hash-filter">No Zero Hashrate</label>
                        <input type="checkbox" id="no-zero-hash-filter">
                    </div>
                    <div class="zero-hash-toggle">
                        <label for="low-hashrate-filter">Low Hashrate</label>
                        <input type="checkbox" id="low-hashrate-filter">
                    </div>
                </div>
            </div>
            <div class="miners-content" id="miners-content">{pods_html}</div>
        </div>
        <div class="panel">
            <div class="panel-header"><span style="color: #00ff00;">Error Filters</span></div>
            <div class="search-box">
                <input type="text" id="error-search-input" class="search-input" placeholder="Filter by code or keyword (e.g., 412, fan, pool)...">
            </div>
            <div class="filter-controls">
                <button class="toggle-all-btn" id="toggle-all">Deselect All</button>
                <label style="display: flex; align-items: center; gap: 6px; color: #ccc; font-size: 0.85rem; margin-top: 10px; cursor: pointer;">
                    <input type="checkbox" id="expanded-only-filter"> Expanded Only
                </label>
            </div>
            <div class="error-filters" id="error-filters">{error_filters_html}</div>
        </div>
        <div class="panel">
            <div class="panel-header"><span style="color: #00ff00;">Copy List</span></div>
            <div class="copy-controls">
                <button class="copy-btn" onclick="copyToClipboard()">Copy All</button>
                <span class="copy-count" id="copy-count">0 items</span>
            </div>
            <textarea id="copy-textarea" class="copy-textarea" readonly placeholder="Filtered miners will appear here in format: [site],[location],[last5serial]"></textarea>
        </div>
    </div>
</div>
<script>
    const allErrorCodes = new Set({all_error_codes_json});
    const errorDetails = {error_details_json};
    const podMinerTypes = {pod_miner_types_json};
    let selectedErrors = new Set(allErrorCodes);
    let filterZeroHashrate = false;
    let filterNoZeroHashrate = false;
    let filterLowHashrate = false;
    let filterExpandedOnly = false;

    function refreshData() {{
        window.location.reload();
    }}

    function getExpandedPodErrors() {{
        const errors = new Set();
        document.querySelectorAll('.pod-section').forEach(pod => {{
            const podMiners = pod.querySelector('.pod-miners');
            if (!podMiners.classList.contains('collapsed')) {{
                podMiners.querySelectorAll('.miner-card').forEach(miner => {{
                    JSON.parse(miner.dataset.errors || '[]').forEach(e => errors.add(e));
                }});
            }}
        }});
        return errors;
    }}

    function updateErrorFilters() {{
        if (!filterExpandedOnly) {{
            document.querySelectorAll('.error-filter-item').forEach(item => {{
                item.style.display = '';
                item.querySelector('.error-count').textContent = item.dataset.globalCount || item.querySelector('.error-count').textContent;
            }});
            return;
        }}
        // Count errors only from expanded pods
        const expandedErrorCounts = {{}};
        document.querySelectorAll('.pod-section').forEach(pod => {{
            const podMiners = pod.querySelector('.pod-miners');
            if (!podMiners.classList.contains('collapsed')) {{
                podMiners.querySelectorAll('.miner-card').forEach(miner => {{
                    JSON.parse(miner.dataset.errors || '[]').forEach(e => {{
                        expandedErrorCounts[e] = (expandedErrorCounts[e] || 0) + 1;
                    }});
                }});
            }}
        }});
        document.querySelectorAll('.error-filter-item').forEach(item => {{
            const code = item.dataset.error;
            // Save global count on first run
            if (!item.dataset.globalCount) {{
                item.dataset.globalCount = item.querySelector('.error-count').textContent;
            }}
            let count = expandedErrorCounts[code] || 0;
            // Check wildcard patterns
            if (count === 0 && code.includes('X')) {{
                const pattern = code.replace(/X/g, '\\\\d');
                const regex = new RegExp(`^${{pattern}}$`);
                for (let [err, cnt] of Object.entries(expandedErrorCounts)) {{
                    if (regex.test(err)) count += cnt;
                }}
            }}
            item.style.display = count > 0 ? '' : 'none';
            item.querySelector('.error-count').textContent = count;
        }});
    }}

    function togglePod(podName) {{
        const podMiners = document.querySelector(`[data-pod-miners="${{podName}}"]`);
        const toggle = document.querySelector(`[data-pod="${{podName}}"] .pod-toggle`);
        podMiners.classList.toggle('collapsed');
        toggle.innerHTML = podMiners.classList.contains('collapsed') ? '&gt;' : 'v';
        updateErrorFilters();
        updateCopyText();
    }}

    function expandAllPods() {{
        const pods = document.querySelectorAll('.pod-section');
        const button = document.getElementById('expand-all-btn');
        const isExpanding = button.textContent === 'Expand All';
        
        pods.forEach(pod => {{
            const podMiners = pod.querySelector('.pod-miners');
            const toggle = pod.querySelector('.pod-toggle');
            
            if (isExpanding) {{
                podMiners.classList.remove('collapsed');
                toggle.innerHTML = 'v';
            }} else {{
                podMiners.classList.add('collapsed');
                toggle.innerHTML = '&gt;';
            }}
        }});
        
        button.textContent = isExpanding ? 'Collapse All' : 'Expand All';
        updateErrorFilters();
        updateCopyText();
    }}

    function updateDisplay() {{
        const miners = document.querySelectorAll('.miner-card');
        const pods = document.querySelectorAll('.pod-section');

        miners.forEach(miner => {{
            const minerErrors = new Set(JSON.parse(miner.dataset.errors || '[]'));
            const minerHashrate = parseFloat(miner.dataset.hashrate);

            // Check against checkbox filters
            const hasSelectedError = [...minerErrors].some(error => {{
                if (selectedErrors.has(error)) return true;
                for (let selected of selectedErrors) {{
                    if (selected.includes('X')) {{
                        const pattern = selected.replace(/X/g, '\\\\d');
                        const regex = new RegExp(`^${{pattern}}$`);
                        if (regex.test(error)) return true;
                    }}
                }}
                return false;
            }});

            // Check against hashrate filters
            let matchesHashrateFilter = true;
            if (filterZeroHashrate) {{
                matchesHashrateFilter = minerHashrate === 0;
            }} else if (filterNoZeroHashrate) {{
                matchesHashrateFilter = minerHashrate > 0;
            }} else if (filterLowHashrate) {{
                // Check if miner hashrate is below threshold for its type
                const minerType = miner.dataset.minerType;
                const minerPod = miner.dataset.pod;
                let threshold = 0;
                
                if (minerType === 'M60') {{
                    threshold = parseFloat(document.getElementById('m60-threshold').value) || 170;
                }} else if (minerType === 'Jpro') {{
                    threshold = parseFloat(document.getElementById('jpro-threshold').value) || 100;
                }} else if (minerType === 'XP') {{
                    threshold = parseFloat(document.getElementById('xp-threshold').value) || 120;
                }}
                
                matchesHashrateFilter = minerHashrate > 0 && minerHashrate < threshold;
            }}

            miner.classList.toggle('hidden', !(hasSelectedError && matchesHashrateFilter));
        }});

        pods.forEach(pod => {{
            const visibleMiners = pod.querySelectorAll('.miner-card:not(.hidden)');
            pod.classList.toggle('hidden', visibleMiners.length === 0);
            const header = pod.querySelector('.pod-header span:last-child');
            const toggleIcon = pod.querySelector('.pod-miners').classList.contains('collapsed') ? '&gt;' : 'v';
            if (header) {{
                header.innerHTML = `(${{visibleMiners.length}} miners) <span class="pod-toggle">${{toggleIcon}}</span>`;
            }}
        }});

        updateCopyText();
    }}

    function updateCopyText() {{
        const visibleMiners = document.querySelectorAll('.miner-card:not(.hidden)');
        const copyList = Array.from(visibleMiners)
            .filter(miner => !miner.closest('.pod-miners').classList.contains('collapsed'))
            .map(miner => `${{miner.dataset.site}},${{miner.dataset.position}},${{miner.dataset.serial}}`);

        const textarea = document.getElementById('copy-textarea');
        textarea.value = copyList.join('\\n');
        document.getElementById('copy-count').textContent = `${{copyList.length}} items`;
    }}

    function copyToClipboard() {{
        const textarea = document.getElementById('copy-textarea');
        navigator.clipboard.writeText(textarea.value).then(() => {{
            const button = document.querySelector('.copy-btn');
            const originalText = button.textContent;
            button.textContent = 'Copied!';
            setTimeout(() => {{ button.textContent = originalText; }}, 1500);
        }});
    }}

    document.getElementById('zero-hash-filter').addEventListener('change', (e) => {{
        filterZeroHashrate = e.target.checked;
        if (filterZeroHashrate) {{
            filterNoZeroHashrate = false;
            filterLowHashrate = false;
            document.getElementById('no-zero-hash-filter').checked = false;
            document.getElementById('low-hashrate-filter').checked = false;
        }}
        updateDisplay();
    }});

    document.getElementById('no-zero-hash-filter').addEventListener('change', (e) => {{
        filterNoZeroHashrate = e.target.checked;
        if (filterNoZeroHashrate) {{
            filterZeroHashrate = false;
            filterLowHashrate = false;
            document.getElementById('zero-hash-filter').checked = false;
            document.getElementById('low-hashrate-filter').checked = false;
        }}
        updateDisplay();
    }});

    document.getElementById('low-hashrate-filter').addEventListener('change', (e) => {{
        filterLowHashrate = e.target.checked;
        if (filterLowHashrate) {{
            filterZeroHashrate = false;
            filterNoZeroHashrate = false;
            document.getElementById('zero-hash-filter').checked = false;
            document.getElementById('no-zero-hash-filter').checked = false;
        }}
        updateDisplay();
    }});

    document.getElementById('error-search-input').addEventListener('input', (e) => {{
        const terms = e.target.value.split(',').map(t => t.trim().toLowerCase()).filter(t => t);
        // First re-apply expanded-only filter to reset visibility and counts
        updateErrorFilters();
        // Then apply search filter on top
        if (terms.length > 0) {{
            document.querySelectorAll('.error-filter-item').forEach(item => {{
                if (item.style.display === 'none') return;  // Already hidden by expanded-only
                const code = item.dataset.error.toLowerCase();
                const meaning = (item.querySelector('.error-meaning')?.textContent || '').toLowerCase();
                const solution = (item.querySelector('.error-solution')?.textContent || '').toLowerCase();
                const matches = terms.some(t => code.includes(t) || meaning.includes(t) || solution.includes(t));
                if (!matches) item.style.display = 'none';
            }});
        }}
    }});

    document.getElementById('toggle-all').addEventListener('click', () => {{
        const button = document.getElementById('toggle-all');
        const allFilters = document.querySelectorAll('.error-filter-item');
        const areAllSelected = selectedErrors.size === allErrorCodes.size;

        selectedErrors = areAllSelected ? new Set() : new Set(allErrorCodes);
        button.textContent = areAllSelected ? 'Select All' : 'Deselect All';

        allFilters.forEach(item => {{
            const isSelected = selectedErrors.has(item.dataset.error);
            item.classList.toggle('selected', isSelected);
            item.querySelector('input[type="checkbox"]').checked = isSelected;
        }});
        updateDisplay();
    }});

    document.querySelectorAll('.error-filter-item').forEach(item => {{
        item.addEventListener('click', (e) => {{
            const checkbox = item.querySelector('input[type="checkbox"]');
            if (e.target !== checkbox) {{
                checkbox.checked = !checkbox.checked;
            }}
            const errorCode = item.dataset.error;
            checkbox.checked ? selectedErrors.add(errorCode) : selectedErrors.delete(errorCode);

            item.classList.toggle('selected', checkbox.checked);
            document.getElementById('toggle-all').textContent = selectedErrors.size === allErrorCodes.size ? 'Deselect All' : 'Deselect All';
            updateDisplay();
        }});
    }});

    document.getElementById('expanded-only-filter').addEventListener('change', (e) => {{
        filterExpandedOnly = e.target.checked;
        updateErrorFilters();
    }});

    // Add event listeners for threshold inputs
    document.getElementById('m60-threshold').addEventListener('input', updateDisplay);
    document.getElementById('jpro-threshold').addEventListener('input', updateDisplay);
    document.getElementById('xp-threshold').addEventListener('input', updateDisplay);

    document.addEventListener('DOMContentLoaded', updateDisplay);
</script>

<script>
{generate_dropdown_js()}
</script>
</body>
</html>"""

def main():
    """Main execution function."""
    hunter = DeadMinerHunter()
    active_miners = hunter.get_active_miners()
    pods = hunter.group_by_pod(active_miners)

    all_errors = set()
    for miner in active_miners:
        all_errors.update(miner['errors'])

    error_info = []
    for error_code in sorted(list(all_errors)):
        info = hunter.get_error_info(error_code)
        miner_count = sum(1 for miner in active_miners if error_code in miner['errors'])
        error_info.append({
            'code': error_code,
            'meaning': info['meaning'],
            'solution': info['solution'],
            'count': miner_count
        })

    # Print the HTTP header and the rendered HTML page
    print("Content-Type: text/html\n")
    print(render_page(hunter, active_miners, pods, error_info))

if __name__ == "__main__":
    main()
