"""HTML report generator for OpenClaw endpoint detection.

Renders the static template with injected endpoint data and writes
a self-contained report folder (HTML, CSS, JS, assets).
"""

import json
import logging
import shutil
import sys
from datetime import datetime
from pathlib import Path
from typing import Any

logger = logging.getLogger(__name__)

if getattr(sys, "frozen", False):
    # Running as a PyInstaller bundle – data files are unpacked under sys._MEIPASS
    TEMPLATE_DIR = Path(getattr(sys, "_MEIPASS")) / "report" / "templates"
else:
    TEMPLATE_DIR = Path(__file__).parent / "templates"

# OS-specific SVG icons (inline for table rows)
_OS_ICONS = {
    "mac": '''<svg class="icon-sm" viewBox="0 0 24 24" fill="currentColor"><path d="M18.71 19.5C17.88 20.74 17 21.95 15.66 21.97C14.32 22 13.89 21.18 12.37 21.18C10.84 21.18 10.37 21.95 9.09997 22C7.78997 22.05 6.79997 20.68 5.95997 19.47C4.24997 17 2.93997 12.45 4.69997 9.39C5.56997 7.87 7.12997 6.91 8.81997 6.88C10.1 6.86 11.32 7.75 12.11 7.75C12.89 7.75 14.37 6.68 15.92 6.84C16.57 6.87 18.39 7.1 19.56 8.82C19.47 8.88 17.39 10.1 17.41 12.63C17.44 15.65 20.06 16.66 20.09 16.67C20.06 16.74 19.67 18.11 18.71 19.5ZM13 3.5C13.73 2.67 14.94 2.04 15.94 2C16.07 3.17 15.6 4.35 14.9 5.19C14.21 6.04 13.07 6.7 11.95 6.61C11.8 5.46 12.36 4.26 13 3.5Z"/></svg>''',
    "win": '''<svg class="icon-sm" viewBox="0 0 24 24" fill="currentColor"><path d="M0 3.449L9.75 2.1v9.451H0m10.949-9.602L24 0v11.4H10.949M0 12.6h9.75v9.451L0 20.699M10.949 12.6H24V24l-12.9-1.801"/></svg>''',
    "linux": '''<svg class="icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="4" width="16" height="16" rx="2" ry="2"></rect><rect x="9" y="9" width="6" height="6"></rect><line x1="9" y1="1" x2="9" y2="4"></line><line x1="15" y1="1" x2="15" y2="4"></line><line x1="9" y1="20" x2="9" y2="23"></line><line x1="15" y1="20" x2="15" y2="23"></line><line x1="20" y1="9" x2="23" y2="9"></line><line x1="20" y1="14" x2="23" y2="14"></line><line x1="1" y1="9" x2="4" y2="9"></line><line x1="1" y1="14" x2="4" y2="14"></line></svg>''',
}
_DEFAULT_OS_ICON = '''<svg class="icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect><line x1="8" y1="21" x2="16" y2="21"></line><line x1="12" y1="17" x2="12" y2="21"></line></svg>'''


def _os_icon(os_name: str) -> str:
    if not os_name:
        return _DEFAULT_OS_ICON
    lower = os_name.lower()
    if "mac" in lower:
        return _OS_ICONS["mac"]
    if "win" in lower:
        return _OS_ICONS["win"]
    if "linux" in lower:
        return _OS_ICONS["linux"]
    return _DEFAULT_OS_ICON


def _escape_html(s: str) -> str:
    if s is None:
        return ""
    return (
        str(s)
        .replace("&", "&amp;")
        .replace("<", "&lt;")
        .replace(">", "&gt;")
        .replace('"', "&quot;")
    )


def _table_row(ep: dict[str, Any]) -> str:
    endpoint_id_raw = str(ep.get("endpoint", ep.get("name", "")))
    username_raw = str(ep.get("username", ep.get("user", _UNKNOWN)))
    drawer_key = f"{endpoint_id_raw}||{username_raw}"
    endpoint_id = _escape_html(endpoint_id_raw)
    endpoint_id_attr = json.dumps(drawer_key)  # safe for use in onclick="openDrawer(...)"
    username = _escape_html(username_raw)
    os_val = _escape_html(str(ep.get("os", ep.get("osType", _UNKNOWN))))
    last_seen = _escape_html(str(ep.get("last_seen", ep.get("lastSeen", _UNKNOWN))))
    icon = _os_icon(ep.get("os", ep.get("osType", "")))
    # Use single quotes for the attribute so inner double quotes in the ID don't break the handler
    return f"""<tr class="table-row" onclick='openDrawer({endpoint_id_attr})'>
                <td>
                  <div class="cell-with-icon">
                    <svg class="icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                      <rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect>
                      <line x1="8" y1="21" x2="16" y2="21"></line>
                      <line x1="12" y1="17" x2="12" y2="21"></line>
                    </svg>
                    <span class="font-medium">{endpoint_id}</span>
                  </div>
                </td>
                <td>
                  <div class="cell-with-icon">
                    <svg class="icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                      <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
                      <circle cx="12" cy="7" r="4"></circle>
                    </svg>
                    <span class="text-muted">{username}</span>
                  </div>
                </td>
                <td>
                  <div class="cell-with-icon">
                    {icon}
                    <span class="text-muted">{os_val}</span>
                  </div>
                </td>
                <td>
                  <div class="cell-with-icon">
                    <svg class="icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                      <circle cx="12" cy="12" r="10"></circle>
                      <polyline points="12 6 12 12 16 14"></polyline>
                    </svg>
                    <span class="text-muted">{last_seen}</span>
                  </div>
                </td>
                <td>
                  <button class="btn-icon">
                    <svg class="icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                      <polyline points="9 18 15 12 9 6"></polyline>
                    </svg>
                  </button>
                </td>
              </tr>"""


_UNKNOWN = "Unknown"


def _to_js_endpoint(ep: dict[str, Any], *, edr_source: str | None = None) -> dict[str, Any]:
    """Convert Python endpoint dict to camelCase for the frontend. Missing fields become 'Unknown'."""
    name = ep.get("endpoint", ep.get("name", ""))
    out = {
        "name": name,
        "user": ep.get("username", ep.get("user", _UNKNOWN)),
        "osType": ep.get("os", ep.get("osType", _UNKNOWN)),
        "lastSeen": ep.get("last_seen", ep.get("lastSeen", _UNKNOWN)),
        "firstDetected": ep.get("first_detected", ep.get("firstDetected", _UNKNOWN)),
        "evidence": ep.get("evidence", _UNKNOWN),
        "edrSource": edr_source if edr_source is not None else ep.get("edr_source", ep.get("edrSource", _UNKNOWN)),
        "status": ep.get("status", _UNKNOWN),
        "skills": ep.get("skills", []),
    }
    ev = ep.get("installation_evidence", ep.get("installationEvidence"))
    if ev is None:
        ev = {}
    commandline = (
        ep.get("commandline")
        or ep.get("command_line")
        or ev.get("commandline")
        or ev.get("command_line")
        or ev.get("found")
        or _UNKNOWN
    )
    out["installationEvidence"] = {
        "commandline": commandline,
        "path": ev.get("path", _UNKNOWN),
        "confidence": ev.get("confidence", _UNKNOWN),
    }
    return out


def render_report(
    endpoints: list[dict[str, Any]],
    output_dir: Path | str,
    *,
    edr_source: str | None = None,
    generated_at: str | None = None,
) -> Path:
    """Render the HTML report from a list of endpoint dicts and write a report folder.

    Args:
        endpoints: List of endpoint dicts. Each dict must include at least:
            - endpoint (or name): hostname
            - username (or user): user name
            - os (or osType): e.g. "macOS", "Windows", "Linux"
            - last_seen (or lastSeen): formatted timestamp string
            Any other fields not provided are shown as "Unknown" in the report.
        output_dir: Directory to create; will contain index.html, script.js,
            styles.css, and assets/. Created if it does not exist.
        edr_source: EDR product name to show for all endpoints (e.g. "CrowdStrike").
            If not provided, per-endpoint edr_source is used or "Unknown".
        generated_at: Report generation timestamp string. Defaults to now.

    Returns:
        Path to the created output directory (same as output_dir, resolved).
    """
    output_dir = Path(output_dir).resolve()
    output_dir.mkdir(parents=True, exist_ok=True)

    count = len(endpoints)
    table_rows = "\n".join(_table_row(ep) for ep in endpoints)
    js_keyed: dict[str, dict[str, Any]] = {}
    for ep in endpoints:
        name = str(ep.get("endpoint", ep.get("name", "")))
        user = str(ep.get("username", ep.get("user", _UNKNOWN)))
        key = f"{name}||{user}"
        js_keyed[key] = _to_js_endpoint(ep, edr_source=edr_source)
    endpoints_json = json.dumps(js_keyed, ensure_ascii=False)
    if generated_at is None:
        generated_at = datetime.now().strftime("%b %d, %Y %H:%M")

    template_path = TEMPLATE_DIR / "index.html"
    html = template_path.read_text(encoding="utf-8")
    html = (
        html.replace("{{TOTAL_COUNT}}", str(count))
        .replace("{{THREATS_COUNT}}", str(count))
        .replace("{{TABLE_ROWS}}", table_rows)
        .replace("{{ENDPOINTS_JSON}}", endpoints_json)
        .replace("{{GENERATED_AT}}", generated_at)
    )

    (output_dir / "index.html").write_text(html, encoding="utf-8")
    shutil.copy(TEMPLATE_DIR / "script.js", output_dir / "script.js")
    shutil.copy(TEMPLATE_DIR / "styles.css", output_dir / "styles.css")
    assets_src = TEMPLATE_DIR / "assets"
    assets_dst = output_dir / "assets"
    if assets_dst.exists():
        shutil.rmtree(assets_dst)
    shutil.copytree(assets_src, assets_dst)

    logger.info("Rendered report with %s endpoints to %s", count, output_dir)
    return output_dir
