#!/usr/bin/python3
"""Create a privacy-filtered Chromium2 HbbTV diagnostic archive."""

from __future__ import print_function

import glob
import os
import re
import shutil
import subprocess
import sys
import tarfile
import tempfile
import time


LOG_DIRECTORY = "/home/root/logs"
PERSISTENT_MARKER = "/home/root/.chromium-hbbtv-debug"
TEMP_MARKER = "/tmp/chromium-hbbtv-debug"
MAX_LOG_BYTES = 8 * 1024 * 1024
SECRET_PATTERNS = (
    (re.compile(r"(?i)([?&](?:access_token|authorization|auth|jwt|session|token)=)[^&#\s]+"), r"\1<redacted>"),
    (re.compile(r"(?i)(Bearer\s+)[A-Za-z0-9._~+/=-]+"), r"\1<redacted>"),
    (re.compile(r"(?i)([\"']?(?:access_token|authorization|auth|jwt|session|token)[\"']?\s*[:=]\s*[\"']?)[^\"'&,\s}]+"), r"\1<redacted>"),
)


def redact(value):
    text = value if isinstance(value, str) else value.decode("utf-8", "replace")
    for pattern, replacement in SECRET_PATTERNS:
        text = pattern.sub(replacement, text)
    return text


def safe_name(value):
    value = re.sub(r"[^A-Za-z0-9._-]+", "-", value.strip())
    return value.strip("-.")[:48] or "snapshot"


def read_tail(path, limit=MAX_LOG_BYTES):
    with open(path, "rb") as source:
        try:
            size = os.fstat(source.fileno()).st_size
            if size > limit:
                source.seek(-limit, os.SEEK_END)
        except (IOError, OSError):
            pass
        return source.read(limit + 1)


def write_text(root, relative, text):
    target = os.path.join(root, relative)
    parent = os.path.dirname(target)
    if parent and not os.path.isdir(parent):
        os.makedirs(parent)
    with open(target, "w", encoding="utf-8") as output:
        output.write(redact(text))
        if text and not text.endswith("\n"):
            output.write("\n")


def copy_text(root, path, relative=None):
    try:
        data = read_tail(path)
    except (IOError, OSError) as error:
        return "unavailable: %s: %s" % (path, error)
    if relative is None:
        relative = path.lstrip("/").replace("/", "_")
    write_text(root, relative, data.decode("utf-8", "replace"))
    return None


def run_command(root, name, arguments, line_filter=None):
    try:
        process = subprocess.Popen(arguments, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
        try:
            output = process.communicate(timeout=8)[0]
        except subprocess.TimeoutExpired:
            process.kill()
            output = process.communicate()[0] + b"\n<command timed out>\n"
        text = output.decode("utf-8", "replace")
        if line_filter is not None:
            text = "\n".join(line for line in text.splitlines() if line_filter(line))
        text += "\n<exit status %d>\n" % process.returncode
    except (IOError, OSError) as error:
        text = "unavailable: %s\n" % error
    write_text(root, os.path.join("commands", name + ".txt"), text)


def collect_settings(root):
    allowed = (
        "config.av.",
        "config.plugins.Chromium.hbbtvdebug=",
        "config.plugins.Chromium.rcudebug=",
        "config.plugins.Chromium.portalvideoheight=",
    )
    try:
        with open("/etc/enigma2/settings", "r", encoding="utf-8", errors="replace") as settings:
            lines = [line for line in settings if line.startswith(allowed)]
        write_text(root, "settings/audio-and-chromium.txt", "".join(lines))
    except (IOError, OSError) as error:
        write_text(root, "settings/audio-and-chromium.txt", "unavailable: %s" % error)


def collect_proc(root):
    fixed_paths = (
        "/proc/asound/cards",
        "/proc/asound/pcm",
        "/proc/meminfo",
        "/proc/swaps",
        "/proc/stb/fb/dst_left",
        "/proc/stb/fb/dst_top",
        "/proc/stb/fb/dst_width",
        "/proc/stb/fb/dst_height",
        "/proc/stb/info/architecture",
        "/proc/stb/info/brand",
        "/proc/stb/info/boxtype",
        "/proc/stb/info/chipset",
        "/proc/stb/info/model",
        "/proc/stb/info/vumodel",
        "/proc/stb/vmpeg/0/dst_left",
        "/proc/stb/vmpeg/0/dst_top",
        "/proc/stb/vmpeg/0/dst_width",
        "/proc/stb/vmpeg/0/dst_height",
        "/sys/class/graphics/fb0/bits_per_pixel",
        "/sys/class/graphics/fb0/name",
        "/sys/class/graphics/fb0/virtual_size",
    )
    paths = list(fixed_paths)
    paths.extend(glob.glob("/proc/stb/audio/*"))
    paths.extend(glob.glob("/proc/stb/hdmi/*"))
    for path in sorted(set(paths)):
        if os.path.isdir(path):
            continue
        relative = os.path.join("state", path.lstrip("/").replace("/", "_")) + ".txt"
        copy_text(root, path, relative)


def collect_logs(root):
    paths = (
        "/home/root/logs/chromium-hbbtv-e2.log",
        "/home/root/logs/chromium-hbbtv-e2.log.1",
        "/home/root/logs/chromium-hbbtv-browser.log",
        "/home/root/logs/chromium-hbbtv-browser.log.1",
        "/home/root/logs/chromium-rcu.log",
        "/home/root/logs/chromium-rcu.log.1",
        "/tmp/openhbbtv-e2backend.log",
        "/tmp/chromium2-hbbtv-stderr.log",
    )
    for path in paths:
        if os.path.isfile(path):
            copy_text(root, path, os.path.join("logs", os.path.basename(path)))

    # Include at most three recent Enigma2 logs. They are filtered again even
    # though the HbbTV logger itself already redacts tokens.
    candidates = set()
    for pattern in ("*enigma2*.log", "*enigma2*debug*", "*e2*.log"):
        candidates.update(glob.glob(os.path.join(LOG_DIRECTORY, pattern)))
    candidates = sorted(candidates, key=lambda path: os.path.getmtime(path), reverse=True)
    for path in candidates[:3]:
        copy_text(root, path, os.path.join("logs", "enigma2-" + os.path.basename(path)))


def marker_enabled():
    if os.path.exists(PERSISTENT_MARKER):
        return True
    try:
        with open("/etc/enigma2/settings", "r") as settings:
            return any(
                line.strip().lower() == "config.plugins.chromium.hbbtvdebug=true"
                for line in settings
            )
    except (IOError, OSError):
        return False


def collect(label):
    if not os.path.isdir(LOG_DIRECTORY):
        os.makedirs(LOG_DIRECTORY)
    label = safe_name(label)
    timestamp = time.strftime("%Y%m%d-%H%M%S")
    work = tempfile.mkdtemp(prefix="chromium-hbbtv-log-")
    archive = os.path.join(LOG_DIRECTORY, "chromium-hbbtv-debug-%s-%s.tar.gz" % (timestamp, label))
    try:
        write_text(
            work,
            "README.txt",
            "Collected: %s\nLabel: %s\nPersistent debug enabled: %s\n"
            % (time.strftime("%Y-%m-%d %H:%M:%S %z"), label, marker_enabled()),
        )
        collect_logs(work)
        collect_settings(work)
        collect_proc(work)
        run_command(work, "date", ["date"])
        run_command(work, "uname", ["uname", "-a"])
        run_command(work, "free", ["free"])
        run_command(work, "df", ["df", "-h"])
        run_command(work, "mount", ["mount"])
        run_command(work, "processes", ["ps"])
        run_command(work, "aplay-devices", ["aplay", "-l"])
        run_command(work, "aplay-pcms", ["aplay", "-L"])
        run_command(work, "amixer", ["amixer"])
        run_command(work, "amixer-card0", ["amixer", "-c", "0"])
        run_command(work, "amixer-card1", ["amixer", "-c", "1"])
        run_command(
            work,
            "packages",
            ["opkg", "list-installed"],
            lambda line: any(word in line.lower() for word in ("chromium", "enigma2", "alsa", "zram", "gles", "egl")),
        )
        run_command(
            work,
            "dmesg-audio-video",
            ["dmesg"],
            lambda line: any(word in line.lower() for word in ("aac", "alsa", "audio", "codec", "decoder", "hdmi", "oom", "out of memory", "video")),
        )
        with tarfile.open(archive, "w:gz") as bundle:
            bundle.add(work, arcname="chromium-hbbtv-debug")
        os.chmod(archive, 0o600)
    finally:
        shutil.rmtree(work, ignore_errors=True)
    print(archive)
    return 0


def usage():
    print("Usage: chromium2-hbbtv-log collect [label]", file=sys.stderr)
    print("       chromium2-hbbtv-log enable|disable|status", file=sys.stderr)
    return 2


def main():
    action = sys.argv[1] if len(sys.argv) > 1 else "collect"
    if action == "collect":
        return collect(sys.argv[2] if len(sys.argv) > 2 else "snapshot")
    if action == "enable":
        if not os.path.isdir(os.path.dirname(PERSISTENT_MARKER)):
            os.makedirs(os.path.dirname(PERSISTENT_MARKER))
        with open(PERSISTENT_MARKER, "a"):
            pass
        print("Persistent HbbTV debug marker enabled; restart HbbTV before reproducing.")
        return 0
    if action == "disable":
        for path in (PERSISTENT_MARKER, TEMP_MARKER):
            try:
                os.unlink(path)
            except OSError:
                pass
        print("Debug markers disabled. A Setup option set to Yes still has priority.")
        return 0
    if action == "status":
        print("enabled" if marker_enabled() else "disabled")
        return 0
    return usage()


if __name__ == "__main__":
    sys.exit(main())
