#!/usr/bin/env python3
"""
Script to extract exceptions from log files containing a specific string.
Supports both individual files and directories.
Generates an interactive HTML report with grouped exceptions.
"""

import re
import sys
import argparse
from pathlib import Path
from collections import defaultdict
from datetime import datetime
import html
import json
import urllib.parse


def is_log_line(line):
    """Check if line matches the standard log format."""
    return bool(re.match(r'^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} \|', line))


def is_error_line(line):
    """Check if line is an ERROR level log line."""
    return ' | ERROR | ' in line and is_log_line(line)


def is_java_exception_line(line):
    """
    Check if line is a Java exception (e.g., java.lang.NullPointerException).
    This typically follows an ERROR line.
    """
    # Common Java exception patterns
    java_exception_patterns = [
        r'^[a-zA-Z][a-zA-Z0-9_.$]*Exception',  # Any exception class
        r'^[a-zA-Z][a-zA-Z0-9_.$]*Error',       # Any error class
        r'^java\.',                              # Java standard exceptions
        r'^android\.',                           # Android exceptions
        r'^kotlin\.',                            # Kotlin exceptions
    ]
    stripped = line.strip()
    return any(re.match(pattern, stripped) for pattern in java_exception_patterns)


def is_stack_trace_line(line):
    """
    Check if line is a stack trace line.
    Typically starts with 'at ' or contains method calls.
    """
    stripped = line.strip()
    return (
        stripped.startswith('at ') or
        stripped.startswith('... ') or
        re.match(r'^\s*at\s+[\w.$]+\.[\w$]+\(', line) or
        'more' in stripped.lower() and re.search(r'\d+\s+more', stripped)
    )


def extract_exception_key(exception_lines):
    """
    Extract a key from exception lines to group similar exceptions.
    Uses the Java exception class name and first line of stack trace.

    Args:
        exception_lines: List of lines forming the exception

    Returns:
        String key for grouping
    """
    if not exception_lines:
        return "Unknown Error"

    # Look for the Java exception class name
    exception_class = None
    exception_message = ""
    first_stack_line = None

    for line in exception_lines:
        stripped = line.strip()

        # Find the exception class (e.g., java.lang.NullPointerException)
        if not exception_class and is_java_exception_line(line):
            # Extract just the exception class and message
            parts = stripped.split(':', 1)
            exception_class = parts[0].strip()
            if len(parts) > 1:
                exception_message = parts[1].strip()
                # Truncate long messages
                if len(exception_message) > 80:
                    exception_message = exception_message[:80] + "..."

        # Get the first stack trace line to add context
        if not first_stack_line and is_stack_trace_line(line):
            # Extract just the method/class info
            at_match = re.search(r'at\s+([\w.$]+\.[\w$]+)', stripped)
            if at_match:
                first_stack_line = at_match.group(1)

    # Build the key
    if exception_class:
        key = exception_class
        if exception_message:
            key = f"{exception_class}: {exception_message}"
        elif first_stack_line:
            key = f"{exception_class} at {first_stack_line}"
        return key

    # Fallback to ERROR message if no Java exception found
    for line in exception_lines:
        if is_error_line(line):
            match = re.search(r'\| ERROR \| (.+)$', line)
            if match:
                message = match.group(1).strip()
                if len(message) > 150:
                    message = message[:150] + "..."
                return message

    return "Unknown Error"


def extract_exceptions(log_file, search_string):
    """
    Extract all Java stack trace exceptions from log file where the file contains the search string.
    Only captures exceptions that have actual Java stack traces.

    Args:
        log_file: Path to the log file
        search_string: String to search for in the file

    Returns:
        List of tuples (exception_lines, exception_key, line_number)
    """
    try:
        with open(log_file, 'r', encoding='utf-8', errors='ignore') as f:
            content = f.read()
    except Exception as e:
        print(f"Warning: Could not read {log_file}: {e}")
        return []

    # Check if search string exists in file
    if search_string not in content:
        return []

    lines = content.splitlines()
    exceptions = []
    current_exception = []
    exception_start_line = 0
    in_exception = False
    has_java_exception = False

    for line_num, line in enumerate(lines, 1):
        if is_error_line(line):
            # Save previous exception if it has a Java stack trace
            if current_exception and has_java_exception:
                key = extract_exception_key(current_exception)
                exceptions.append((current_exception, key, exception_start_line))

            # Start of a new potential exception
            current_exception = [line]
            exception_start_line = line_num
            in_exception = True
            has_java_exception = False

        elif in_exception:
            # Check if this is a Java exception or stack trace
            if is_java_exception_line(line):
                has_java_exception = True
                current_exception.append(line)
            elif is_stack_trace_line(line):
                has_java_exception = True
                current_exception.append(line)
            elif line.strip() and (line.startswith('\t') or line.startswith('    ')):
                # Indented line, might be part of exception
                current_exception.append(line)
            elif line.strip().startswith('Caused by:'):
                # Chained exception
                has_java_exception = True
                current_exception.append(line)
            elif not is_log_line(line) and line.strip() and has_java_exception:
                # Non-log line that's part of the exception
                current_exception.append(line)
            else:
                # End of exception block
                if current_exception and has_java_exception:
                    key = extract_exception_key(current_exception)
                    exceptions.append((current_exception, key, exception_start_line))
                current_exception = []
                in_exception = False
                has_java_exception = False

    # Don't forget the last exception if file ends with one
    if current_exception and has_java_exception:
        key = extract_exception_key(current_exception)
        exceptions.append((current_exception, key, exception_start_line))

    return exceptions


def get_log_files(path):
    """
    Get all log files from a path (file or directory).

    Args:
        path: Path object to file or directory

    Returns:
        List of Path objects for log files
    """
    if path.is_file():
        return [path]
    elif path.is_dir():
        # Common log file extensions
        log_extensions = ['.log', '.txt', '.out']
        log_files = []

        # Recursively search for log files
        for ext in log_extensions:
            log_files.extend(path.rglob(f'*{ext}'))

        # Also include files without extension that might be logs
        for file in path.rglob('*'):
            if file.is_file() and not file.suffix and file not in log_files:
                log_files.append(file)

        return sorted(log_files)
    else:
        return []


def group_exceptions(files_with_exceptions):
    """
    Group exceptions by their error message across all files.

    Args:
        files_with_exceptions: List of tuples (log_file, exceptions)
            where exceptions is a list of (exception_lines, key, line_number)

    Returns:
        Dictionary mapping exception keys to list of (log_file, exception_lines, line_number)
    """
    grouped = defaultdict(list)

    for log_file, exceptions in files_with_exceptions:
        for exception_lines, key, line_number in exceptions:
            grouped[key].append((log_file, exception_lines, line_number))

    return grouped


def generate_html_report(grouped_exceptions, search_string, output_file, log_path):
    """
    Generate an interactive HTML report with grouped exceptions.

    Args:
        grouped_exceptions: Dictionary from group_exceptions()
        search_string: The search string used
        output_file: Path to output HTML file
        log_path: Path to the log directory/file being analyzed
    """
    path = log_path
    # Sort groups by number of occurrences (descending)
    sorted_groups = sorted(
        grouped_exceptions.items(),
        key=lambda x: len(x[1]),
        reverse=True
    )

    html_content = f"""<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Java Exception Report - {html.escape(search_string)}</title>
    <style>
        * {{
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }}
        body {{
            font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
            line-height: 1.6;
            color: #b0b0b0;
            background: linear-gradient(135deg, #0a0a0a 0%, #1a1a1a 25%, #0f0f0f 50%, #000000 100%);
            background-attachment: fixed;
            height: 100vh;
            overflow: hidden;
        }}
        .header {{
            background: linear-gradient(135deg, #0f0f0f 0%, #1a1a1a 50%, #252525 100%);
            color: #d0d0d0;
            padding: 1rem 1.5rem;
            box-shadow: 0 20px 40px rgba(0, 0, 0, 0.8);
            border-bottom: 2px solid #3a3a3a;
            position: relative;
        }}
        .header::before {{
            content: '';
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: repeating-linear-gradient(
                45deg,
                transparent,
                transparent 2px,
                rgba(60, 60, 60, 0.1) 2px,
                rgba(60, 60, 60, 0.1) 4px
            );
            opacity: 0.3;
        }}
        .header h1 {{
            font-size: 1.5rem;
            margin-bottom: 0.25rem;
            position: relative;
            z-index: 1;
            text-transform: uppercase;
            letter-spacing: 2px;
            text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
            color: #e0e0e0;
            font-weight: 700;
        }}
        .header .meta {{
            opacity: 0.9;
            font-size: 0.85rem;
            position: relative;
            z-index: 1;
            text-transform: uppercase;
            font-weight: 500;
            letter-spacing: 1px;
            color: #808080;
        }}
        .stats-bar {{
            background: #0a0a0a;
            color: #e0e0e0;
            padding: 0.75rem 1.5rem;
            display: flex;
            gap: 2rem;
            border-bottom: 2px solid #3a3a3a;
        }}
        .stat-item {{
            display: flex;
            align-items: center;
            gap: 0.5rem;
        }}
        .stat-item .label {{
            color: #909090;
            font-size: 0.85rem;
            text-transform: uppercase;
            letter-spacing: 1px;
            font-weight: 600;
        }}
        .stat-item .value {{
            color: #808080;
            font-weight: bold;
            font-size: 1.1rem;
            font-family: 'Consolas', monospace;
            text-shadow: 0 0 10px rgba(128, 128, 128, 0.5);
        }}
        .main-container {{
            display: flex;
            height: calc(100vh - 100px);
            overflow: hidden;
        }}
        .master-panel {{
            width: 400px;
            background: linear-gradient(145deg, #1a1a1a, #252525);
            border-right: 2px solid #3a3a3a;
            overflow-y: auto;
            flex-shrink: 0;
        }}
        .master-header {{
            padding: 1rem 1.5rem;
            background: linear-gradient(145deg, #0f0f0f, #1a1a1a);
            border-bottom: 2px solid #3a3a3a;
            position: sticky;
            top: 0;
            z-index: 10;
            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);
        }}
        .master-header h2 {{
            color: #d0d0d0;
            font-size: 1rem;
            font-weight: 700;
            margin-bottom: 0.75rem;
            text-transform: uppercase;
            letter-spacing: 1px;
        }}
        .search-controls {{
            display: flex;
            flex-direction: column;
            gap: 0.5rem;
        }}
        .search-input-group {{
            display: flex;
            gap: 0.5rem;
        }}
        .search-input {{
            flex: 1;
            background: #0f0f0f;
            border: 1px solid #3a3a3a;
            color: #c0c0c0;
            padding: 0.5rem 0.75rem;
            border-radius: 2px;
            font-size: 0.85rem;
            font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
            box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.4);
        }}
        .search-input:focus {{
            outline: none;
            border-color: #808080;
            box-shadow: 0 0 10px rgba(128, 128, 128, 0.3);
        }}
        .search-input::placeholder {{
            color: #606060;
        }}
        .btn {{
            background: linear-gradient(145deg, #505050, #6a6a6a);
            color: #d0d0d0;
            border: 1px solid #4a4a4a;
            padding: 0.5rem 1rem;
            border-radius: 2px;
            font-size: 0.85rem;
            cursor: pointer;
            transition: all 0.3s ease;
            font-weight: 600;
            text-transform: uppercase;
            letter-spacing: 0.5px;
            font-family: 'Consolas', monospace;
            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);
        }}
        .btn:hover {{
            background: linear-gradient(145deg, #6a6a6a, #808080);
            box-shadow: 0 10px 30px rgba(0, 0, 0, 0.6);
            transform: translateY(-1px);
        }}
        .btn:active {{
            transform: translateY(0px);
            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
        }}
        .btn-secondary {{
            background: linear-gradient(145deg, #2a2a2a, #3a3a3a);
            border-color: #3a3a3a;
        }}
        .btn-secondary:hover {{
            background: linear-gradient(145deg, #3a3a3a, #4a4a4a);
        }}
        .filter-info {{
            color: #808080;
            font-size: 0.75rem;
            margin-top: 0.25rem;
            font-family: 'Consolas', monospace;
            text-transform: uppercase;
            letter-spacing: 0.5px;
        }}
        .exception-list {{
            list-style: none;
        }}
        .exception-item {{
            border-bottom: 1px solid #2a2a2a;
            cursor: pointer;
            transition: all 0.3s ease;
            background: linear-gradient(145deg, #1a1a1a, #252525);
        }}
        .exception-item:hover {{
            background: linear-gradient(145deg, #252525, #2f2f2f);
            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
        }}
        .exception-item.active {{
            background: linear-gradient(145deg, #2a2a2a, #353535);
            border-left: 3px solid #808080;
            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.6);
        }}
        .exception-item.hidden {{
            display: none;
        }}
        .exception-item-content {{
            padding: 1rem 1.5rem;
        }}
        .exception-title {{
            color: #c0c0c0;
            font-size: 0.9rem;
            font-weight: 500;
            margin-bottom: 0.5rem;
            line-height: 1.4;
            word-break: break-word;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
        }}
        .exception-meta {{
            display: flex;
            gap: 1rem;
            font-size: 0.8rem;
            color: #808080;
        }}
        .exception-badge {{
            background: linear-gradient(145deg, #505050, #6a6a6a);
            color: #d0d0d0;
            padding: 0.15rem 0.5rem;
            border-radius: 2px;
            font-size: 0.75rem;
            font-weight: 600;
            border: 1px solid #4a4a4a;
            box-shadow: 0 0 10px rgba(80, 80, 80, 0.3);
            font-family: 'Consolas', monospace;
        }}
        .detail-panel {{
            flex: 1;
            background: #0a0a0a;
            overflow-y: auto;
            display: flex;
            flex-direction: column;
        }}
        .detail-empty {{
            display: flex;
            align-items: center;
            justify-content: center;
            height: 100%;
            color: #606060;
            font-size: 1.1rem;
            text-transform: uppercase;
            letter-spacing: 2px;
            font-family: 'Consolas', monospace;
        }}
        .detail-content {{
            display: none;
            flex-direction: column;
            height: 100%;
        }}
        .detail-content.active {{
            display: flex;
        }}
        .detail-header {{
            background: linear-gradient(145deg, #0f0f0f, #1a1a1a);
            padding: 1.5rem;
            border-bottom: 2px solid #3a3a3a;
            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);
        }}
        .detail-header h2 {{
            color: #d0d0d0;
            font-size: 1.2rem;
            margin-bottom: 0.75rem;
            line-height: 1.4;
            font-weight: 700;
            text-transform: uppercase;
            letter-spacing: 1px;
            font-family: 'Consolas', monospace;
        }}
        .detail-header .meta {{
            color: #909090;
            font-size: 0.9rem;
            text-transform: uppercase;
            letter-spacing: 1px;
            font-weight: 600;
        }}
        .occurrences {{
            flex: 1;
            overflow-y: auto;
            padding: 1rem;
        }}
        .occurrence {{
            background: linear-gradient(145deg, #1a1a1a, #252525);
            border-radius: 2px;
            margin-bottom: 1rem;
            border: 1px solid #3a3a3a;
            overflow: hidden;
            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5),
                        inset 0 1px 0 rgba(255, 255, 255, 0.05);
        }}
        .occurrence-header {{
            background: linear-gradient(145deg, #0f0f0f, #1a1a1a);
            padding: 0.75rem 1rem;
            display: flex;
            justify-content: space-between;
            align-items: center;
            border-bottom: 2px solid #3a3a3a;
        }}
        .occurrence-file {{
            color: #c0c0c0;
            font-size: 0.85rem;
            font-weight: 600;
            font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
            cursor: pointer;
            text-decoration: none;
            transition: all 0.2s ease;
            text-transform: uppercase;
            letter-spacing: 0.5px;
        }}
        .occurrence-file:hover {{
            color: #808080;
            text-shadow: 0 0 10px rgba(128, 128, 128, 0.5);
        }}
        .occurrence-line {{
            color: #808080;
            font-size: 0.8rem;
            background: #0f0f0f;
            padding: 0.25rem 0.75rem;
            border-radius: 2px;
            border: 1px solid #3a3a3a;
            font-family: 'Consolas', monospace;
        }}
        .stack-trace {{
            background: #1a1a1a;
            color: #d4d4d4;
            padding: 1rem;
            font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
            font-size: 0.8rem;
            line-height: 1.6;
            overflow-x: auto;
        }}
        .stack-trace pre {{
            margin: 0;
            white-space: pre-wrap;
            word-wrap: break-word;
        }}
        .stack-trace .error-line {{
            color: #f48771;
        }}
        .stack-trace .exception-class {{
            color: #ff6b6b;
            font-weight: bold;
        }}
        .stack-trace .at-line {{
            color: #a9b7c6;
        }}
        .stack-trace .method {{
            color: #ffc66d;
        }}
        .stack-trace .file-info {{
            color: #6897bb;
        }}
        .file-viewer-modal {{
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: rgba(0, 0, 0, 0.9);
            z-index: 10000;
            display: none;
            flex-direction: column;
        }}
        .file-viewer-modal.active {{
            display: flex;
        }}
        .file-viewer-header {{
            background: linear-gradient(145deg, #0f0f0f, #1a1a1a);
            padding: 1rem 1.5rem;
            display: flex;
            justify-content: space-between;
            align-items: center;
            border-bottom: 2px solid #3a3a3a;
            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);
        }}
        .file-viewer-header h3 {{
            color: #d0d0d0;
            font-size: 1rem;
            font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
            margin: 0;
            font-weight: 700;
            text-transform: uppercase;
            letter-spacing: 1px;
        }}
        .file-viewer-controls {{
            display: flex;
            gap: 0.5rem;
        }}
        .file-viewer-btn {{
            background: linear-gradient(145deg, #2a2a2a, #3a3a3a);
            color: #d0d0d0;
            border: 1px solid #3a3a3a;
            padding: 0.5rem 1rem;
            border-radius: 2px;
            cursor: pointer;
            font-size: 0.85rem;
            transition: all 0.3s ease;
            font-weight: 600;
            text-transform: uppercase;
            letter-spacing: 0.5px;
            font-family: 'Consolas', monospace;
            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);
        }}
        .file-viewer-btn:hover {{
            background: linear-gradient(145deg, #3a3a3a, #4a4a4a);
            transform: translateY(-1px);
            box-shadow: 0 10px 30px rgba(0, 0, 0, 0.6);
        }}
        .file-viewer-btn.primary {{
            background: linear-gradient(145deg, #505050, #6a6a6a);
            border-color: #4a4a4a;
        }}
        .file-viewer-btn.primary:hover {{
            background: linear-gradient(145deg, #6a6a6a, #808080);
        }}
        .file-viewer-content {{
            flex: 1;
            overflow: auto;
            background: #1a1a1a;
            padding: 1rem;
        }}
        .file-viewer-line {{
            font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
            font-size: 0.8rem;
            line-height: 1.5;
            color: #d4d4d4;
            white-space: pre;
        }}
        .file-viewer-line.highlight {{
            background: #3a3a2a;
            border-left: 3px solid #ffc66d;
            padding-left: 0.5rem;
        }}
        .file-viewer-line-number {{
            display: inline-block;
            width: 60px;
            color: #666;
            user-select: none;
            text-align: right;
            margin-right: 1rem;
        }}
        .file-viewer-loading {{
            display: flex;
            align-items: center;
            justify-content: center;
            height: 100%;
            color: #888;
            font-size: 1.1rem;
        }}
        ::-webkit-scrollbar {{
            width: 10px;
            height: 10px;
        }}
        ::-webkit-scrollbar-track {{
            background: #1e1e1e;
        }}
        ::-webkit-scrollbar-thumb {{
            background: #3a3a3a;
            border-radius: 5px;
        }}
        ::-webkit-scrollbar-thumb:hover {{
            background: #4a4a4a;
        }}
    </style>
</head>
<body>
    <div class="header">
        <h1>Java Exception Report</h1>
        <div class="meta">Search String: <strong>{html.escape(search_string)}</strong> | Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</div>
    </div>

    <div class="stats-bar">
        <div class="stat-item">
            <span class="label">Unique Exceptions:</span>
            <span class="value">{len(sorted_groups)}</span>
        </div>
        <div class="stat-item">
            <span class="label">Total Occurrences:</span>
            <span class="value">{sum(len(occurrences) for _, occurrences in sorted_groups)}</span>
        </div>
        <div class="stat-item">
            <span class="label">Files Affected:</span>
            <span class="value">{len(set(log_file for _, occurrences in sorted_groups for log_file, _, _ in occurrences))}</span>
        </div>
    </div>

    <div class="main-container">
        <div class="master-panel">
            <div class="master-header">
                <h2>Exception Groups (<span id="visible-count">{len(sorted_groups)}</span>/<span id="total-count">{len(sorted_groups)}</span>)</h2>
                <div class="search-controls">
                    <div class="search-input-group">
                        <input type="text"
                               id="filter-input"
                               class="search-input"
                               placeholder="Filter exceptions..."
                               onkeyup="filterExceptions()">
                        <button class="btn btn-secondary" onclick="clearFilter()">Clear</button>
                    </div>
                    <div class="search-input-group">
                        <input type="text"
                               id="search-string"
                               class="search-input"
                               placeholder="New search string (e.g., PINKY)"
                               value="{html.escape(search_string)}">
                        <button class="btn" onclick="reloadWithSearch()">Reload</button>
                    </div>
                    <div class="filter-info" id="filter-info"></div>
                </div>
            </div>
            <ul class="exception-list" id="exception-list">
"""

    # Add master panel items
    for idx, (key, occurrences) in enumerate(sorted_groups):
        files_affected = len(set(log_file for log_file, _, _ in occurrences))
        html_content += f"""                <li class="exception-item" data-group="{idx}" onclick="showDetail({idx})">
                    <div class="exception-item-content">
                        <div class="exception-title">{html.escape(key)}</div>
                        <div class="exception-meta">
                            <span class="exception-badge">{len(occurrences)}</span>
                            <span>{files_affected} file{'s' if files_affected != 1 else ''}</span>
                        </div>
                    </div>
                </li>
"""

    html_content += """            </ul>
        </div>

        <div class="detail-panel">
            <div class="detail-empty">Select an exception to view details</div>
"""

    # Add detail panels
    for idx, (key, occurrences) in enumerate(sorted_groups):
        files_affected = len(set(log_file for log_file, _, _ in occurrences))
        html_content += f"""
            <div class="detail-content" id="detail-{idx}">
                <div class="detail-header">
                    <h2>{html.escape(key)}</h2>
                    <div class="meta">{len(occurrences)} occurrence(s) across {files_affected} file(s)</div>
                </div>
                <div class="occurrences">
"""

        for log_file, exception_lines, line_number in occurrences:
            # Syntax highlighting for stack trace
            highlighted_trace = ""
            for line in exception_lines:
                line_html = html.escape(line)
                # Skip the ERROR log line for cleaner display
                if is_error_line(line):
                    continue
                highlighted_trace += line_html + "\\n"

            # Create file:line URL for opening in editor
            file_path_abs = str(log_file.absolute())
            file_uri = f"file://{file_path_abs}"

            html_content += f"""
                    <div class="occurrence">
                        <div class="occurrence-header">
                            <a class="occurrence-file" href="#" onclick="openFile('{html.escape(file_path_abs)}', {line_number}); return false;" title="{html.escape(file_path_abs)}">{html.escape(str(log_file.name))}</a>
                            <div class="occurrence-line">Line {line_number}</div>
                        </div>
                        <div class="stack-trace">
                            <pre>{highlighted_trace.rstrip()}</pre>
                        </div>
                    </div>
"""

        html_content += """                </div>
            </div>
"""

    html_content += """        </div>
    </div>

    <!-- File Viewer Modal -->
    <div class="file-viewer-modal" id="file-viewer-modal">
        <div class="file-viewer-header">
            <h3 id="file-viewer-title">Loading...</h3>
            <div class="file-viewer-controls">
                <button class="file-viewer-btn primary" onclick="copyFilePath()">Copy Path</button>
                <button class="file-viewer-btn" onclick="closeFileViewer()">Close (Esc)</button>
            </div>
        </div>
        <div class="file-viewer-content" id="file-viewer-content">
            <div class="file-viewer-loading">Loading file...</div>
        </div>
    </div>

    <script>
        // Store current file info for copying
        let currentFilePath = '';
        let currentLineNumber = 0;
"""

    # Add file content data as JSON
    file_contents = {}
    for _, occurrences in sorted_groups:
        for log_file, _, _ in occurrences:
            file_path_str = str(log_file.absolute())
            if file_path_str not in file_contents:
                try:
                    with open(log_file, 'r', encoding='utf-8', errors='ignore') as f:
                        lines = f.readlines()
                        # Limit to reasonable size (e.g., 10000 lines)
                        if len(lines) > 10000:
                            lines = lines[:10000]
                        file_contents[file_path_str] = lines
                except Exception as e:
                    file_contents[file_path_str] = [f"Error reading file: {e}"]

    # Escape and embed file contents
    html_content += "        const fileContents = " + json.dumps(file_contents) + ";\n\n"

    html_content += """        function showDetail(groupId) {
            // Hide all detail panels
            document.querySelectorAll('.detail-content').forEach(panel => {
                panel.classList.remove('active');
            });

            // Hide empty message
            document.querySelector('.detail-empty').style.display = 'none';

            // Show selected detail panel
            const detailPanel = document.getElementById('detail-' + groupId);
            if (detailPanel) {
                detailPanel.classList.add('active');
            }

            // Update active state in master list
            document.querySelectorAll('.exception-item').forEach(item => {
                item.classList.remove('active');
            });
            const selectedItem = document.querySelector('[data-group="' + groupId + '"]');
            if (selectedItem) {
                selectedItem.classList.add('active');
            }
        }

        function filterExceptions() {
            const filterInput = document.getElementById('filter-input');
            const filterText = filterInput.value.toLowerCase();
            const items = document.querySelectorAll('.exception-item');
            let visibleCount = 0;

            items.forEach(item => {
                const title = item.querySelector('.exception-title').textContent.toLowerCase();
                if (title.includes(filterText)) {
                    item.classList.remove('hidden');
                    visibleCount++;
                } else {
                    item.classList.add('hidden');
                }
            });

            // Update counter
            document.getElementById('visible-count').textContent = visibleCount;

            // Update filter info
            const filterInfo = document.getElementById('filter-info');
            if (filterText) {
                filterInfo.textContent = `Filtering by: "${filterInput.value}"`;
            } else {
                filterInfo.textContent = '';
            }

            // Auto-select first visible item if current selection is hidden
            const activeItem = document.querySelector('.exception-item.active');
            if (!activeItem || activeItem.classList.contains('hidden')) {
                const firstVisible = document.querySelector('.exception-item:not(.hidden)');
                if (firstVisible) {
                    const groupId = firstVisible.getAttribute('data-group');
                    showDetail(parseInt(groupId));
                }
            }
        }

        function clearFilter() {
            document.getElementById('filter-input').value = '';
            filterExceptions();
        }

        function reloadWithSearch() {
            const searchString = document.getElementById('search-string').value.trim();
            if (!searchString) {
                alert('Please enter a search string');
                return;
            }

            // Build the command to show the user
            const pathStr = '{html.escape(str(path.absolute()))}';
            const command = './cli/sw_log_parser.py "' + pathStr + '" "' + searchString + '"';

            if (confirm('Reload logs with search string: "' + searchString + '"?\\n\\nRun this command in your terminal:\\n\\n' + command)) {
                // Copy command to clipboard
                navigator.clipboard.writeText(command).then(() => {
                    alert('Command copied to clipboard!\\n\\nPaste it in your terminal to reload.');
                }).catch(err => {
                    alert('Command to run:\\n\\n' + command);
                });
            }
        }

        function openFile(filePath, lineNumber) {
            currentFilePath = filePath;
            currentLineNumber = lineNumber;

            // Show modal
            const modal = document.getElementById('file-viewer-modal');
            modal.classList.add('active');

            // Update title
            const fileName = filePath.split('/').pop();
            document.getElementById('file-viewer-title').textContent = fileName;

            // Load file content
            const contentDiv = document.getElementById('file-viewer-content');

            if (fileContents[filePath]) {
                const lines = fileContents[filePath];
                let html = '';

                for (let i = 0; i < lines.length; i++) {
                    const lineNum = i + 1;
                    const isHighlight = lineNum === lineNumber;
                    const lineClass = isHighlight ? 'file-viewer-line highlight' : 'file-viewer-line';
                    const escapedLine = lines[i]
                        .replace(/&/g, '&amp;')
                        .replace(/</g, '&lt;')
                        .replace(/>/g, '&gt;')
                        .replace(/\\n$/, ''); // Remove trailing newline for display

                    html += '<div class="' + lineClass + '" id="line-' + lineNum + '">';
                    html += '<span class="file-viewer-line-number">' + lineNum + '</span>';
                    html += escapedLine;
                    html += '</div>';
                }

                contentDiv.innerHTML = html;

                // Scroll to highlighted line
                setTimeout(() => {
                    const highlightedLine = document.getElementById('line-' + lineNumber);
                    if (highlightedLine) {
                        highlightedLine.scrollIntoView({ behavior: 'smooth', block: 'center' });
                    }
                }, 100);
            } else {
                contentDiv.innerHTML = '<div class="file-viewer-loading">File not found in embedded data</div>';
            }
        }

        function closeFileViewer() {
            document.getElementById('file-viewer-modal').classList.remove('active');
        }

        function copyFilePath() {
            navigator.clipboard.writeText(currentFilePath).then(() => {
                const btn = event.target;
                const originalText = btn.textContent;
                btn.textContent = 'Copied!';
                setTimeout(() => {
                    btn.textContent = originalText;
                }, 2000);
            }).catch(err => {
                alert('Could not copy to clipboard');
            });
        }

        // Close modal with Escape key
        document.addEventListener('keydown', function(e) {
            if (e.key === 'Escape') {
                closeFileViewer();
            }
        });

        // Close modal when clicking outside
        document.getElementById('file-viewer-modal').addEventListener('click', function(e) {
            if (e.target === this) {
                closeFileViewer();
            }
        });

        // Auto-select first item if available
        if (document.querySelector('.exception-item')) {
            showDetail(0);
        }

        // Enable Enter key for filter
        document.getElementById('filter-input').addEventListener('keypress', function(e) {
            if (e.key === 'Enter') {
                filterExceptions();
            }
        });

        // Enable Enter key for reload
        document.getElementById('search-string').addEventListener('keypress', function(e) {
            if (e.key === 'Enter') {
                reloadWithSearch();
            }
        });
    </script>
</body>
</html>
"""

    # Write the HTML file
    with open(output_file, 'w', encoding='utf-8') as f:
        f.write(html_content)

    print(f"\nHTML report generated: {output_file}")
    print(f"Open in browser: file://{output_file.absolute()}")


def main():
    parser = argparse.ArgumentParser(
        description='Extract and analyze exceptions from log files containing a specific string.',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog='''
Examples:
  %(prog)s /path/to/app.log PINKY
  %(prog)s /path/to/logs PINKY -o report.html
  %(prog)s /path/to/logs "Connection timeout" --output-dir ./reports
        '''
    )

    parser.add_argument(
        'path',
        nargs='?',
        default='/Users/matthewroberts/development/projects/somewear/logs/android',
        help='Path to log file or directory (default: %(default)s)'
    )

    parser.add_argument(
        'search_string',
        nargs='?',
        default='callsign=PARTS',
        help='String to search for in log files (default: %(default)s)'
    )

    parser.add_argument(
        '-o', '--output',
        help='Output HTML file path (default: exceptions_report.html in current directory)',
        default='/Users/matthewroberts/development/projects/somewear/celilo/exceptions_report.html'
    )

    parser.add_argument(
        '--output-dir',
        help='Directory to save the HTML report (default: current directory)'
    )

    parser.add_argument(
        '--no-console',
        action='store_true',
        help='Skip console output, only generate HTML report'
    )

    args = parser.parse_args()

    path = Path(args.path)

    if not path.exists():
        print(f"Error: Path '{args.path}' not found")
        sys.exit(1)

    print(f"Searching for exceptions containing string '{args.search_string}'...")

    log_files = get_log_files(path)

    if not log_files:
        print(f"No log files found in '{args.path}'")
        sys.exit(1)

    print(f"Found {len(log_files)} log file(s) to search\n")

    total_exceptions = 0
    files_with_exceptions = []

    for log_file in log_files:
        exceptions = extract_exceptions(log_file, args.search_string)

        if exceptions:
            files_with_exceptions.append((log_file, exceptions))
            total_exceptions += len(exceptions)

    if not files_with_exceptions:
        print(f"No exceptions found containing '{args.search_string}'")
        return

    print(f"Found {total_exceptions} exception(s) in {len(files_with_exceptions)} file(s)\n")

    # Group exceptions
    grouped = group_exceptions(files_with_exceptions)

    # Determine output file path
    if args.output:
        output_file = Path(args.output)
    else:
        output_dir = Path(args.output_dir) if args.output_dir else Path.cwd()
        output_file = output_dir / 'exceptions_report.html'

    # Ensure output directory exists
    output_file.parent.mkdir(parents=True, exist_ok=True)

    # Generate HTML report
    generate_html_report(grouped, args.search_string, output_file, path)

    # Console output (unless --no-console is specified)
    if not args.no_console:
        print("\n" + "=" * 80)
        print("CONSOLE SUMMARY")
        print("=" * 80)

        # Sort groups by occurrence count
        sorted_groups = sorted(
            grouped.items(),
            key=lambda x: len(x[1]),
            reverse=True
        )

        for key, occurrences in sorted_groups:
            print(f"\n[{len(occurrences)} occurrences] {key}")
            print("-" * 80)

            # Show first occurrence as an example
            if occurrences:
                log_file, exception_lines, line_number = occurrences[0]
                print(f"Example from: {log_file}:{line_number}")
                for line in exception_lines[:10]:  # Show first 10 lines
                    print(line)
                if len(exception_lines) > 10:
                    print(f"... ({len(exception_lines) - 10} more lines)")

            # List all affected files
            affected_files = set(log_file for log_file, _, _ in occurrences)
            if len(affected_files) > 1:
                print(f"\nAlso found in {len(affected_files) - 1} other file(s):")
                for log_file in list(affected_files)[1:6]:  # Show up to 5 more files
                    print(f"  - {log_file}")
                if len(affected_files) > 6:
                    print(f"  ... and {len(affected_files) - 6} more")

        print("\n" + "=" * 80)


if __name__ == "__main__":
    main()