#!/usr/bin/env python3

import configparser
import os
import select
import sqlite3
import subprocess
from pathlib import Path
import sys
import termios
import time
import tty
from datetime import datetime, timezone


DB = "/opt/arm/state/arm.db"
CONFIG = "/opt/arm/config/arm.conf"
RIP_ROOT = "/rips/ripping"

RESET = "\033[0m"
BOLD = "\033[1m"
DIM = "\033[2m"
GREEN = "\033[32m"
YELLOW = "\033[33m"
RED = "\033[31m"
CYAN = "\033[36m"
BLUE = "\033[34m"
MAGENTA = "\033[35m"


# ---------------------------------------------------------------------------
# Utility helpers
# ---------------------------------------------------------------------------

def run(cmd, timeout=3):
    try:
        return subprocess.run(
            cmd,
            stdout=subprocess.PIPE,
            stderr=subprocess.DEVNULL,
            text=True,
            timeout=timeout,
            check=False,
        ).stdout.strip()
    except Exception:
        return ""


def clear():
    print("\033[2J\033[H", end="")


def status_text(status):
    if status in ("RIPPING", "TRANSCODING", "TITLE_SELECTED"):
        return f"{CYAN}{status}{RESET}"

    if status in (
        "RIP_FAILED",
        "TRANSCODE_FAILED",
        "SELECTION_FAILED",
    ):
        return f"{RED}{status}{RESET}"

    if status == "LIBRARY_MOVED":
        return f"{GREEN}{status}{RESET}"

    return f"{YELLOW}{status}{RESET}"


def parse_time(value):
    if not value:
        return None

    try:
        return datetime.fromisoformat(value)
    except Exception:
        return None


def elapsed(value):
    dt = parse_time(value)

    if not dt:
        return ""

    seconds = max(
        0,
        int(
            (
                datetime.now(timezone.utc) - dt
            ).total_seconds()
        ),
    )

    hours, remainder = divmod(seconds, 3600)
    minutes, seconds = divmod(remainder, 60)

    return f"{hours:02}:{minutes:02}:{seconds:02}"


def bytes_text(value):
    value = float(value)

    for unit in ("B", "KB", "MB", "GB", "TB"):
        if value < 1024:
            return f"{value:.1f} {unit}"

        value /= 1024

    return f"{value:.1f} PB"


def directory_size(path):
    total = 0

    if not os.path.exists(path):
        return 0

    for root, dirs, files in os.walk(path):
        for filename in files:
            try:
                total += os.path.getsize(
                    os.path.join(root, filename)
                )
            except OSError:
                pass

    return total


# ---------------------------------------------------------------------------
# Library helpers
# ---------------------------------------------------------------------------

def library_status(conn):
    """
    Return compact TV and movie library information for the dashboard.

    TV is grouped by show/season and only displays episodes that have
    actually reached the Jellyfin library.

    Movies are intentionally limited to the most recent relevant movie
    job so the normal dashboard stays within one terminal screen.
    """

    tv = {}

    rows = conn.execute("""
        SELECT
            jt.name,
            jt.season_number,
            jt.episode_number,
            jf.status AS file_status,
            jf.library_file,
            j.destination
        FROM job_titles jt
        JOIN jobs j
            ON j.id = jt.job_id
        LEFT JOIN job_files jf
            ON jf.job_title_id = jt.id
        WHERE j.media_type = 'TV'
          AND jt.season_number IS NOT NULL
          AND jt.episode_number IS NOT NULL
        ORDER BY
            jt.season_number,
            jt.episode_number
    """).fetchall()

    for row in rows:
        show = row["name"]

        # Existing ARM TV title rows may not contain the show name.
        # The library path is authoritative once the file has reached
        # Jellyfin, for example:
        #   /mnt/jellyfin/TV Shows/Game of Thrones/Season 01/...
        if not show and row["library_file"]:
            parts = Path(row["library_file"]).parts

            try:
                shows_index = parts.index("TV Shows")
                if shows_index + 1 < len(parts):
                    show = parts[shows_index + 1]
            except ValueError:
                pass

        if not show and row["destination"]:
            parts = Path(row["destination"]).parts

            try:
                shows_index = parts.index("TV Shows")
                if shows_index + 1 < len(parts):
                    show = parts[shows_index + 1]
            except ValueError:
                pass

        if not show:
            continue

        season = int(row["season_number"])
        episode = int(row["episode_number"])

        key = (show, season)

        if key not in tv:
            tv[key] = {}

        status = row["file_status"]

        if status == "LIBRARY_MOVED":
            tv[key][episode] = "✓"
        elif status in (
            "TRANSCODING",
            "RIPPING",
            "MOVING",
        ):
            tv[key][episode] = "●"
        elif status in (
            "RIPPED",
            "TRANSCODED",
        ):
            # Completed the current stage and waiting for the next
            # pipeline stage.
            tv[key][episode] = "○"
        elif status:
            tv[key][episode] = "✗"
        else:
            # The title exists and is expected, but no job_file has
            # reached an active/failed/library state yet.
            tv[key][episode] = "○"

    # Prefer the newest movie that is actively moving through the
    # pipeline. This keeps the compact dashboard focused on live work.
    movie = conn.execute("""
        SELECT
            id,
            title,
            disc_label,
            status,
            destination,
            updated_at
        FROM jobs
        WHERE COALESCE(media_type, '') != 'TV'
          AND status IN (
              'RIPPING',
              'TRANSCODING',
              'MOVING',
              'TITLE_SELECTED'
          )
        ORDER BY id DESC
        LIMIT 1
    """).fetchone()

    # If nothing is actively processing, show the newest movie waiting
    # between completed pipeline stages.
    if movie is None:
        movie = conn.execute("""
            SELECT
                id,
                title,
                disc_label,
                status,
                destination,
                updated_at
            FROM jobs
            WHERE COALESCE(media_type, '') != 'TV'
              AND status IN (
                  'RIPPED',
                  'TRANSCODED'
              )
            ORDER BY id DESC
            LIMIT 1
        """).fetchone()

    # Finally, fall back to the most recently completed library move.
    if movie is None:
        movie = conn.execute("""
            SELECT
                id,
                title,
                disc_label,
                status,
                destination,
                updated_at
            FROM jobs
            WHERE COALESCE(media_type, '') != 'TV'
              AND status = 'LIBRARY_MOVED'
            ORDER BY id DESC
            LIMIT 1
        """).fetchone()

    return tv, movie


# ---------------------------------------------------------------------------
# Process monitoring
# ---------------------------------------------------------------------------

def process_lines():
    output = run([
        "ps",
        "-eo",
        "pid,etime,%cpu,%mem,cmd",
        "--sort=-%cpu",
    ])

    wanted = (
        "makemkvcon",
        "HandBrakeCLI",
        "ripper.py",
        "transcoder.py",
    )

    results = []
    seen = set()

    for line in output.splitlines():
        lower = line.lower()

        if not any(
            name.lower() in lower
            for name in wanted
        ):
            continue

        if "grep" in lower:
            continue

        parts = line.split(None, 1)

        if not parts:
            continue

        pid = parts[0]

        if pid in seen:
            continue

        seen.add(pid)
        results.append(line)

    return results


def process_summary():
    processes = process_lines()

    makemkv = []
    handbrake = []

    for line in processes:
        lower = line.lower()

        if "makemkvcon" in lower:
            makemkv.append(line)

        elif "handbrakecli" in lower:
            handbrake.append(line)

    return makemkv, handbrake, processes


# ---------------------------------------------------------------------------
# Drive helpers
# ---------------------------------------------------------------------------

def configured_drives():
    config = configparser.ConfigParser()
    config.read(CONFIG)

    drives = []

    if "optical" in config:
        for key, value in config["optical"].items():
            if key.startswith("drive_"):
                drives.append(value)

    return drives


def drive_jobs(jobs):
    result = {
        device: []
        for device in configured_drives()
    }

    for row in jobs:
        if row["drive"] in result:
            result[row["drive"]].append(row)

    return result


def drive_media_state(device):
    """
    Determine whether the optical drive currently has media.

    We use lsblk rather than ARM's database because the database
    describes jobs, while this checks the physical drive.
    """
    output = run([
        "lsblk",
        "-n",
        "-o",
        "TYPE",
        device,
    ])

    if output:
        return "MEDIA"

    return "EMPTY"


def eject_drive(device):
    try:
        result = subprocess.run(
            ["eject", device],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
            check=False,
        )
    except Exception as exc:
        return False, str(exc)

    if result.returncode == 0:
        return True, f"Ejected {device}"

    message = (
        result.stderr.strip()
        or result.stdout.strip()
        or f"eject returned {result.returncode}"
    )

    return False, message


# ---------------------------------------------------------------------------
# Keyboard handling
# ---------------------------------------------------------------------------

def read_key(timeout=2):
    """
    Read exactly one key without requiring ENTER.

    Returns:
        None  - no key pressed
        str   - single key
    """
    ready, _, _ = select.select(
        [sys.stdin],
        [],
        [],
        timeout,
    )

    if not ready:
        return None

    return sys.stdin.read(1)


def eject_menu():
    """
    Interactive single-key eject menu.

    e -> enter this menu
    0 -> eject sr0
    1 -> eject sr1
    q/ESC -> cancel
    """
    clear()

    print(
        f"{CYAN}{BOLD}"
        "╔══════════════════════════════════════════════════════════════════════════════╗"
        f"{RESET}"
    )

    print(
        f"{CYAN}{BOLD}"
        "║                            EJECT DRIVE                                     ║"
        f"{RESET}"
    )

    print(
        f"{CYAN}"
        "╚══════════════════════════════════════════════════════════════════════════════╝"
        f"{RESET}"
    )

    print()
    print("Press:")
    print()
    print(f"  {BOLD}0{RESET}  Eject /dev/sr0")
    print(f"  {BOLD}1{RESET}  Eject /dev/sr1")
    print(f"  {BOLD}ESC{RESET} Cancel")
    print()

    key = read_key(timeout=10)

    if key is None:
        return

    if key == "\x1b":
        return

    if key.lower() == "q":
        return

    if key not in ("0", "1"):
        return

    device = f"/dev/sr{key}"

    clear()

    print()
    print(
        f"{YELLOW}{BOLD}"
        f"Eject {device}? "
        f"{RESET}"
    )

    print()
    print("Press Y to confirm, or any other key to cancel.")

    confirm = read_key(timeout=10)

    if confirm is None:
        return

    if confirm.lower() != "y":
        return

    clear()

    print()
    print(f"Ejecting {device}...")
    print()

    ok, message = eject_drive(device)

    if ok:
        print(f"{GREEN}✓ {message}{RESET}")
    else:
        print(f"{RED}✗ {message}{RESET}")

    print()
    print("Returning to dashboard...")

    time.sleep(1.5)



# ---------------------------------------------------------------------------
# Job correction / deletion
# ---------------------------------------------------------------------------

JOB_CORRECTOR = "/opt/arm/scripts/job_corrector.py"


def job_action_menu(action, old_settings):
    """
    Ask for a Job ID and launch the standalone correction tool.

    arm-status normally uses cbreak keyboard input, while the
    corrector uses normal line input. Restore the terminal while
    the corrector is active, then return to cbreak mode.
    """

    termios.tcsetattr(
        sys.stdin,
        termios.TCSADRAIN,
        old_settings,
    )

    try:
        clear()

        if action == "correct":
            print(f"{BOLD}CORRECT JOB{RESET}")
        else:
            print(f"{BOLD}DELETE JOB{RESET}")

        print()
        print(
            "Enter the ARM Job ID. "
            "Use arm-status --all to find older jobs."
        )
        print()

        value = input(
            "Job ID (blank to cancel): "
        ).strip()

        if not value:
            return

        try:
            job_id = int(value)
        except ValueError:
            print()
            print(f"{RED}Invalid Job ID.{RESET}")
            input("Press ENTER to return...")
            return

        if action == "delete":
            command = [
                "sudo",
                JOB_CORRECTOR,
                "--delete",
                str(job_id),
            ]
        else:
            command = [
                "sudo",
                JOB_CORRECTOR,
                str(job_id),
            ]

        clear()

        subprocess.run(
            command,
            check=False,
        )

        print()
        input(
            "Press ENTER to return to ARM status..."
        )

    finally:
        tty.setcbreak(sys.stdin.fileno())


def auto_eject_enabled():
    """
    Return the persistent [automation] auto_eject setting.
    """
    config = configparser.ConfigParser()
    config.read(CONFIG)

    if not config.has_section("automation"):
        return False

    try:
        return config["automation"].getboolean(
            "auto_eject",
            fallback=False,
        )
    except ValueError:
        return False


def toggle_auto_eject():
    """
    Toggle [automation] auto_eject and persist it to arm.conf.
    Returns the new boolean state.
    """
    config = configparser.ConfigParser()
    config.optionxform = str
    config.read(CONFIG)

    if not config.has_section("automation"):
        config.add_section("automation")

    try:
        current = config["automation"].getboolean(
            "auto_eject",
            fallback=False,
        )
    except ValueError:
        current = False

    new_value = not current

    config["automation"]["auto_eject"] = (
        "true" if new_value else "false"
    )

    with open(CONFIG, "w") as handle:
        config.write(handle)

    return new_value


# ---------------------------------------------------------------------------
# Compact dashboard helpers
# ---------------------------------------------------------------------------

COMPACT_WIDTH = 100


def compact_rule():
    print("-" * COMPACT_WIDTH)


def progress_color(percent):
    """
    Progress color intentionally does NOT use red.

    Red is reserved exclusively for failures/problems.
    """
    if percent is None:
        return DIM

    if percent >= 100:
        return GREEN

    if percent >= 75:
        return MAGENTA

    if percent >= 50:
        return BLUE

    if percent >= 25:
        return CYAN

    return DIM


def rip_progress(conn, job_id):
    """
    Estimate whole-job ripping progress from:
        bytes currently present in /rips/ripping/job-N
        divided by
        total expected size of selected MakeMKV titles.
    """
    row = conn.execute(
        """
        SELECT COALESCE(SUM(size_bytes), 0)
        FROM job_titles
        WHERE job_id = ?
          AND selected = 1
        """,
        (job_id,),
    ).fetchone()

    expected = int(row[0] or 0)

    rip_dir = Path(RIP_ROOT) / f"job-{job_id}"
    actual = directory_size(str(rip_dir))

    if expected <= 0:
        return None, actual, expected

    percent = int((actual / expected) * 100)

    # While actively ripping, avoid claiming completion before
    # the ripper has changed database state.
    percent = max(0, min(percent, 100))

    return percent, actual, expected


def progress_text(percent, growing=False):
    if percent is None:
        return f"{DIM} --%{RESET}"

    color = progress_color(percent)

    arrow = " ↑" if growing else ""

    return (
        f"{color}"
        f"{percent:3d}%"
        f"{arrow:<2}"
        f"{RESET}"
    )


def display_name(value):
    """
    Pretty dashboard-only display name.

    This does not alter the database or library naming.
    """
    if not value:
        return "UNKNOWN"

    name = value.strip()

    # Remove common leading/trailing junk from physical disc labels.
    name = name.strip("_ ")

    # Remove trailing #1234-style mastering/catalog suffixes.
    import re

    name = re.sub(
        r"[_\s]+#\d+\s*$",
        "",
        name,
    )

    name = name.replace("_", " ")

    name = re.sub(
        r"\s+",
        " ",
        name,
    ).strip()

    # Leave explicit job titles alone if already human-readable;
    # otherwise title-case raw disc labels.
    if name.isupper():
        name = name.title()

    return name


def compact_job_status(status):
    if status == "LIBRARY_MOVED":
        return f"{GREEN}✓ MOVED{RESET}"

    if status in (
        "RIP_FAILED",
        "TRANSCODE_FAILED",
        "SELECTION_FAILED",
        "LIBRARY_MOVE_FAILED",
    ):
        return f"{RED}✗ {status}{RESET}"

    if status in (
        "RIPPING",
        "TRANSCODING",
        "TITLE_SELECTED",
        "INSPECTED",
    ):
        return f"{CYAN}● {status}{RESET}"

    if status in ("RIPPED", "TRANSCODED"):
        return f"{YELLOW}○ {status}{RESET}"

    if status == "SUPERSEDED":
        return f"{DIM}○ SUPERSEDED{RESET}"

    return f"{YELLOW}○ {status}{RESET}"


def gpu_status():
    """
    Return a short GPU status string.

    NVIDIA failure/absence is non-fatal to the dashboard.
    """
    output = run(
        [
            "nvidia-smi",
            "--query-gpu="
            "utilization.gpu,"
            "utilization.encoder,"
            "temperature.gpu,"
            "memory.used,"
            "memory.total",
            "--format=csv,noheader,nounits",
        ],
        timeout=2,
    )

    if not output:
        return "NVENC --   GPU --   TEMP --"

    first = output.splitlines()[0]

    parts = [
        part.strip()
        for part in first.split(",")
    ]

    if len(parts) < 5:
        return "NVENC --   GPU --   TEMP --"

    gpu, enc, temp, mem_used, mem_total = parts[:5]

    return (
        f"NVENC {enc}%   "
        f"GPU {gpu}%   "
        f"{temp}°C   "
        f"VRAM {mem_used}/{mem_total} MiB"
    )


def transcode_queue_depth(conn):
    """
    Count jobs that are actually eligible to enter the single
    transcode worker.

    This deliberately mirrors the scheduler's job-level RIPPED state.
    A TV episode that finishes ripping while the rest of its disc is
    still ripping is not yet considered queued.
    """
    row = conn.execute(
        """
        SELECT COUNT(*)
        FROM jobs
        WHERE status = 'RIPPED'
        """
    ).fetchone()

    return int(row[0] or 0)


def transcode_progress(job_id):
    """
    Read the newest HandBrake transcode log for this job and return
    the latest reported encoding percentage.

    Works for movie and TV log naming because we search by job ID.
    """
    import glob
    import re

    patterns = [
        f"/opt/arm/logs/job-{job_id}*transcode*.log",
        f"/opt/arm/logs/job-{job_id}*.log",
    ]

    candidates = []

    for pattern in patterns:
        candidates.extend(glob.glob(pattern))

    candidates = list(set(candidates))

    if not candidates:
        return None

    candidates.sort(
        key=lambda filename: Path(filename).stat().st_mtime,
        reverse=True,
    )

    log = Path(candidates[0])

    try:
        # HandBrake progress uses carriage returns in some builds,
        # so reading the complete text is more reliable than tailing
        # by newline.
        content = log.read_text(
            errors="replace",
        )
    except Exception:
        return None

    matches = re.findall(
        r"Encoding:\s+task\s+\d+\s+of\s+\d+,\s+"
        r"([0-9]+(?:\.[0-9]+)?)\s*%",
        content,
    )

    if not matches:
        return None

    try:
        return float(matches[-1])
    except ValueError:
        return None


def active_transcode_text(conn, previous_sizes):
    job = conn.execute(
        """
        SELECT
            id,
            disc_label,
            title,
            media_type,
            transcode_started_at
        FROM jobs
        WHERE status = 'TRANSCODING'
        ORDER BY id
        LIMIT 1
        """
    ).fetchone()

    if not job:
        return f"{DIM}○ IDLE{RESET}"

    label = display_name(
        job["title"]
        or job["disc_label"]
        or "UNKNOWN"
    )

    episode_text = ""

    if job["media_type"] == "TV":
        episode = conn.execute(
            """
            SELECT season_number, episode_number
            FROM job_files
            WHERE job_id = ?
              AND status = 'TRANSCODING'
            ORDER BY episode_number
            LIMIT 1
            """,
            (job["id"],),
        ).fetchone()

        if episode:
            episode_text = (
                f" S{int(episode['season_number']):02d}"
                f"E{int(episode['episode_number']):02d}"
            )

    percent = transcode_progress(job["id"])

    progress = ""

    if percent is not None:
        key = f"transcode-{job['id']}"
        previous = previous_sizes.get(key)

        growing = (
            previous is not None
            and percent > previous
        )

        previous_sizes[key] = percent

        # Keep one decimal internally but display a clean integer.
        display_percent = int(percent)

        progress = (
            progress_text(
                display_percent,
                growing,
            )
            + " "
        )

    age = elapsed(job["transcode_started_at"])

    return (
        f"{CYAN}● ACTIVE{RESET}  "
        f"{progress}"
        f"{label[:30]}"
        f"{episode_text}  "
        f"[{age}]"
    )

def episode_ranges(numbers):
    numbers = sorted(set(numbers))

    if not numbers:
        return ""

    ranges = []
    start = previous = numbers[0]

    for number in numbers[1:]:
        if number == previous + 1:
            previous = number
            continue

        if start == previous:
            ranges.append(f"E{start:02d}")
        else:
            ranges.append(
                f"E{start:02d}-E{previous:02d}"
            )

        start = previous = number

    if start == previous:
        ranges.append(f"E{start:02d}")
    else:
        ranges.append(
            f"E{start:02d}-E{previous:02d}"
        )

    return ",".join(ranges)


def compact_tv_job_progress(conn, job_id):
    """
    Return episode-level progress for one TV job.

    Example:
        E01-E05 ✓TX   E06 ●TX   E07-E08 ○RIP
    """

    rows = conn.execute(
        """
        SELECT
            jt.episode_number,
            COALESCE(
                (
                    SELECT jf.status
                    FROM job_files jf
                    WHERE jf.job_id = jt.job_id
                      AND (
                            jf.job_title_id = jt.id
                            OR (
                                jf.job_title_id IS NULL
                                AND jf.season_number = jt.season_number
                                AND jf.episode_number = jt.episode_number
                            )
                          )
                    ORDER BY jf.id DESC
                    LIMIT 1
                ),
                'WAITING'
            ) AS file_status
        FROM job_titles jt
        WHERE jt.job_id = ?
          AND jt.selected = 1
          AND jt.episode_number IS NOT NULL
        ORDER BY jt.episode_number
        """,
        (job_id,),
    ).fetchall()

    if not rows:
        return ""

    labels = {
        "LIBRARY_MOVED": "✓LIB",
        "TRANSCODED": "✓TX",
        "TRANSCODING": "●TX",
        "RIPPED": "○RIP",
        "RIPPING": "●RIP",
        "PENDING": "WAIT",
        "WAITING": "WAIT",
        "LIBRARY_MOVE_FAILED": "✗MOVE",
        "TRANSCODE_FAILED": "✗TX",
        "RIP_FAILED": "✗RIP",
    }

    groups = []
    current_status = None
    current_numbers = []

    for row in rows:
        ep = int(row["episode_number"])
        status = row["file_status"] or "WAITING"

        if (
            current_status is None
            or status == current_status
        ):
            current_status = status
            current_numbers.append(ep)
            continue

        groups.append(
            (current_status, current_numbers)
        )

        current_status = status
        current_numbers = [ep]

    if current_numbers:
        groups.append(
            (current_status, current_numbers)
        )

    parts = []

    for status, numbers in groups:
        ranges = episode_ranges(numbers)

        if ranges:
            parts.append(
                f"{ranges} "
                f"{labels.get(status, status)}"
            )

    return "   ".join(parts)


def compact_tv_library(conn):
    tv, _ = library_status(conn)

    lines = []

    for (show, season), episodes in sorted(
        tv.items(),
        key=lambda item: (
            item[0][0].lower(),
            item[0][1],
        ),
    ):
        episode_numbers = sorted(episodes)

        if not episode_numbers:
            continue

        failure = any(
            value == "✗"
            for value in episodes.values()
        )

        active = any(
            value == "●"
            for value in episodes.values()
        )

        # Detect internal gaps only. We cannot know whether episodes
        # before the first or after the last should exist.
        expected = set(
            range(
                episode_numbers[0],
                episode_numbers[-1] + 1,
            )
        )

        missing = expected - set(episode_numbers)

        if failure:
            flag = f"{RED}✗{RESET}"
        elif missing:
            flag = f"{YELLOW}⚠{RESET}"
        elif active:
            flag = f"{CYAN}●{RESET}"
        else:
            flag = " "

        summary = episode_ranges(episode_numbers)

        lines.append(
            (
                show,
                season,
                flag,
                summary,
            )
        )

    return lines


def recent_movies(conn, limit=3):
    return conn.execute(
        """
        SELECT
            COALESCE(NULLIF(title, ''), disc_label) AS name
        FROM jobs
        WHERE COALESCE(media_type, '') != 'TV'
          AND status = 'LIBRARY_MOVED'
        ORDER BY id DESC
        LIMIT ?
        """,
        (limit,),
    ).fetchall()


def raid_space_text():
    try:
        stats = os.statvfs("/mnt/jellyfin")

        total = stats.f_blocks * stats.f_frsize
        free = stats.f_bavail * stats.f_frsize
        used = total - free

        percent = (
            int((used / total) * 100)
            if total
            else 0
        )

        return (
            f"RAID {percent}% used   "
            f"{bytes_text(free)} free"
        )

    except Exception:
        return "RAID unavailable"


def active_inspections():
    """
    Return optical drives currently owned by disc_inspector.py.

    Using the inspector process itself is more reliable than trying
    to infer inspection from generic makemkvcon command lines.
    """
    output = run([
        "ps",
        "-eo",
        "args",
    ])

    drives = []

    for line in output.splitlines():
        if "disc_inspector.py" not in line:
            continue

        # Do not accidentally count this dashboard/process search.
        if "grep" in line:
            continue

        for device in configured_drives():
            if device in line and device not in drives:
                drives.append(device)

    return drives

def render_compact(
    conn,
    service_indicator,
    now,
    previous_sizes,
):
    # ------------------------------------------------------------
    # Banner
    # ------------------------------------------------------------

    print(
        f"{CYAN}{BOLD}"
        "╔══════════════════════════════════════════════════════════════════════════════════════════════════╗"
        f"{RESET}"
    )

    print(
        f"{CYAN}{BOLD}"
        "║                              ARM • AUTOMATED RIPPING MACHINE                                    ║"
        f"{RESET}"
    )

    print(
        f"{CYAN}"
        "╚══════════════════════════════════════════════════════════════════════════════════════════════════╝"
        f"{RESET}"
    )

    print(
        f"{service_indicator}   "
        f"{now.strftime('%H:%M:%S %Z')}"
    )

    # ------------------------------------------------------------
    # Drives / activity
    # ------------------------------------------------------------

    print()
    print(f"{BOLD}ACTIVITY{RESET}")
    compact_rule()

    active_statuses = (
        "INSPECTED",
        "TITLE_SELECTED",
        "RIPPING",
        "RIPPED",
        "TRANSCODING",
        "TRANSCODED",
    )

    drive_parts = []
    inspections = active_inspections()

    for index, device in enumerate(configured_drives()):

        # Inspection owns the physical drive before a job necessarily
        # exists in the database.
        if device in inspections:
            part = (
                f"D{index} {CYAN}● INSPECTING{RESET} "
                f"{DIM}MakeMKV scan{RESET}"
            )

            drive_parts.append(part)
            continue

        # Only these states still require the physical optical disc.
        #
        # Once the job reaches RIPPED, the data is safely on disk and
        # the user may replace the disc while transcoding/library work
        # continues independently.
        current = conn.execute(
            """
            SELECT
                id,
                disc_label,
                title,
                status
            FROM jobs
            WHERE drive = ?
              AND status IN (
                  'INSPECTED',
                  'TITLE_SELECTED',
                  'RIPPING'
              )
            ORDER BY id DESC
            LIMIT 1
            """,
            (device,),
        ).fetchone()

        if current and current["status"] == "RIPPING":
            percent, actual, expected = rip_progress(
                conn,
                current["id"],
            )

            key = f"job-{current['id']}"
            old_size = previous_sizes.get(key)
            growing = (
                old_size is not None
                and actual > old_size
            )

            previous_sizes[key] = actual

            label = display_name(
                current["title"]
                or current["disc_label"]
                or "UNKNOWN"
            )

            part = (
                f"D{index} {CYAN}● RIP{RESET} "
                f"{progress_text(percent, growing)} "
                f"{label[:26]}"
            )

        elif current:
            label = display_name(
                current["title"]
                or current["disc_label"]
                or "UNKNOWN"
            )

            part = (
                f"D{index} "
                f"{compact_job_status(current['status'])} "
                f"{label[:24]}"
            )

        else:
            media = drive_media_state(device)

            if media == "MEDIA":
                part = (
                    f"D{index} {GREEN}✓ REPLACE{RESET} "
                    f"{DIM}[E] eject{RESET}"
                )
            else:
                part = (
                    f"D{index} {DIM}○ EMPTY{RESET}"
                )

        drive_parts.append(part)

    if len(drive_parts) >= 2:
        print(
            f"{drive_parts[0]:<55}"
            f"{drive_parts[1]}"
        )
    else:
        for part in drive_parts:
            print(part)

    queue_depth = transcode_queue_depth(conn)

    print(
        "TX "
        + active_transcode_text(
            conn,
            previous_sizes,
        )
        + f"   Q {queue_depth} waiting   "
        + gpu_status()
    )

    inspections = active_inspections()

    if inspections:
        labels = []

        for device in inspections:
            if device.startswith("/dev/sr"):
                labels.append(
                    "D" + device.rsplit("sr", 1)[1]
                )
            else:
                labels.append(device)

        print(
            f"INSPECT {CYAN}●{RESET} "
            + ", ".join(labels)
            + "  identifying new disc"
        )
    else:
        print(
            f"INSPECT {DIM}○ IDLE{RESET}"
        )

    # ------------------------------------------------------------
    # Recent jobs
    # ------------------------------------------------------------

    print()
    print(f"{BOLD}RECENT JOBS{RESET}")
    compact_rule()

    recent = conn.execute(
        """
        SELECT
            id,
            drive,
            disc_label,
            title,
            media_type,

            status
        FROM jobs
        ORDER BY id DESC
        LIMIT 6
        """
    ).fetchall()

    for row in recent:
        name = display_name(
            row["title"]
            or row["disc_label"]
            or "UNKNOWN"
        )

        drive = (
            row["drive"]
            .replace("/dev/", "")
            if row["drive"]
            else "-"
        )

        print(
            f"{row['id']:<4} "
            f"{drive:<4} "
            f"{name[:38]:<39} "
            f"{compact_job_status(row['status'])}"
        )

        if row["media_type"] == "TV":
            progress = compact_tv_job_progress(
                conn,
                row["id"],
            )

            if progress:
                print(
                    f"          {DIM}"
                    f"{progress}"
                    f"{RESET}"
                )


    # ------------------------------------------------------------
    # Library
    # ------------------------------------------------------------

    print()
    print(f"{BOLD}LIBRARY{RESET}")
    compact_rule()

    tv_lines = compact_tv_library(conn)

    if tv_lines:
        for show, season, flag, summary in tv_lines[:4]:
            print(
                f"TV  {show[:28]:<29} "
                f"S{season:02d} {flag} "
                f"{summary}"
            )
    else:
        print(f"{DIM}TV  No library entries yet.{RESET}")

    movies = recent_movies(conn, 3)

    movie_names = [
        display_name(row["name"])
        for row in movies
        if row["name"]
    ]

    if movie_names:
        print(
            "MOV "
            + " | ".join(
                name[:25]
                for name in movie_names
            )
        )

    # ------------------------------------------------------------
    # Storage / controls
    # ------------------------------------------------------------

    print()
    compact_rule()

    print(
        f"{raid_space_text()}   "
        f"|   {BOLD}[E]{RESET} Eject   "
        f"{BOLD}[A]{RESET} Auto-eject "
        f"{GREEN + 'ON' + RESET if auto_eject_enabled() else DIM + 'OFF' + RESET}   "
        f"{BOLD}[C]{RESET} Correct   "
        f"{BOLD}[D]{RESET} Delete   "
        f"{BOLD}[Q]{RESET} Quit   "
        f"|   {DIM}arm-status --all for diagnostics{RESET}"
    )


# ---------------------------------------------------------------------------
# Main dashboard
# ---------------------------------------------------------------------------

def job_episode_summary(conn, job_id):
    """
    Return selected TV episode ranges grouped by season.
    Example: S01 E01-E05, E07
    """
    rows = conn.execute(
        """
        SELECT
            season_number,
            episode_number
        FROM job_titles
        WHERE job_id = ?
          AND selected = 1
          AND season_number IS NOT NULL
          AND episode_number IS NOT NULL
        ORDER BY season_number, episode_number
        """,
        (job_id,),
    ).fetchall()

    if not rows:
        return None

    by_season = {}

    for row in rows:
        season = int(row["season_number"])
        episode = int(row["episode_number"])

        by_season.setdefault(
            season,
            [],
        ).append(episode)

    parts = []

    for season in sorted(by_season):
        numbers = sorted(
            set(by_season[season])
        )

        ranges = []
        start = previous = numbers[0]

        for number in numbers[1:]:
            if number == previous + 1:
                previous = number
                continue

            if start == previous:
                ranges.append(
                    f"E{start:02d}"
                )
            else:
                ranges.append(
                    f"E{start:02d}-E{previous:02d}"
                )

            start = previous = number

        if start == previous:
            ranges.append(
                f"E{start:02d}"
            )
        else:
            ranges.append(
                f"E{start:02d}-E{previous:02d}"
            )

        parts.append(
            f"S{season:02d} "
            + ",".join(ranges)
        )

    return " | ".join(parts)


def render_jobs_history():
    """
    Print the complete ARM job history once and return.
    """
    with sqlite3.connect(DB) as conn:
        conn.row_factory = sqlite3.Row

        rows = conn.execute(
            """
            SELECT
                id,
                drive,
                disc_label,
                title,
                media_type,
                status,
                destination,
                error_code
            FROM jobs
            ORDER BY id
            """
        ).fetchall()

        print()
        print(
            f"{BOLD}ARM JOB HISTORY{RESET}"
        )
        print("-" * 118)

        print(
            f"{'ID':<6}"
            f"{'TYPE':<8}"
            f"{'STATUS':<24}"
            f"{'DRIVE':<8}"
            f"{'LABEL':<34}"
            f"TITLE"
        )

        print("-" * 118)

        if not rows:
            print("No ARM jobs found.")
            return

        for row in rows:
            drive = (
                row["drive"].replace("/dev/", "")
                if row["drive"]
                else "-"
            )

            label = (
                row["disc_label"]
                or "-"
            )

            title = (
                row["title"]
                or "-"
            )

            print(
                f"{row['id']:<6}"
                f"{str(row['media_type'] or '-'):<8}"
                f"{str(row['status'] or '-'):<24}"
                f"{drive:<8}"
                f"{label[:33]:<34}"
                f"{title}"
            )

            if row["media_type"] == "TV":
                episodes = job_episode_summary(
                    conn,
                    row["id"],
                )

                if episodes:
                    print(
                        f"      Episodes:    {episodes}"
                    )

            print(
                "      Destination: "
                f"{row['destination'] or '-'}"
            )

            if row["error_code"]:
                print(
                    "      Error:       "
                    f"{row['error_code']}"
                )

            print()

        print("-" * 118)
        print(
            f"{len(rows)} job(s)"
        )


def main():
    args = {
        arg.lower()
        for arg in sys.argv[1:]
    }

    show_jobs = (
        "--jobs" in args
        or "-jobs" in args
    )

    show_all = (
        "--all" in args
        or "-all" in args
    )

    if show_jobs:
        render_jobs_history()
        return 0
    previous_sizes = {}

    # Put terminal into cbreak mode so single keys work.
    old_settings = termios.tcgetattr(sys.stdin)

    try:
        tty.setcbreak(sys.stdin.fileno())

        while True:
            clear()

            now = datetime.now().astimezone()

            service = run([
                "systemctl",
                "is-active",
                "arm.service",
            ])

            service_indicator = (
                f"{GREEN}● ACTIVE{RESET}"
                if service == "active"
                else f"{RED}● "
                f"{service.upper() or 'UNKNOWN'}"
                f"{RESET}"
            )

            if show_all:
                print(
                    f"{CYAN}{BOLD}"
                    "╔══════════════════════════════════════════════════════════════════════════════╗"
                    f"{RESET}"
                )

                print(
                    f"{CYAN}{BOLD}"
                    "║                         ARM MACHINE STATUS                                 ║"
                    f"{RESET}"
                )

                print(
                    f"{CYAN}"
                    "╚══════════════════════════════════════════════════════════════════════════════╝"
                    f"{RESET}"
                )

                print()
                print(
                    f"ARM service: {service_indicator}"
                )

                print(
                    "Last refresh:",
                    now.strftime(
                        "%Y-%m-%d %H:%M:%S %Z"
                    ),
                )

            try:
                with sqlite3.connect(DB) as conn:

                    conn.row_factory = sqlite3.Row

                    jobs = conn.execute("""
                        SELECT
                            id,
                            drive,
                            disc_label,
                            status,
                            rip_started_at,
                            rip_completed_at,
                            transcode_started_at,
                            transcode_completed_at,
                            destination,
                            error_code,
                            error_message
                        FROM jobs
                        ORDER BY id DESC
                        LIMIT 5
                    """).fetchall()

                    drive_map = drive_jobs(jobs)

                    if not show_all:
                        render_compact(
                            conn,
                            service_indicator,
                            now,
                            previous_sizes,
                        )

                        key = read_key(timeout=2)

                        if key:
                            if key.lower() == "q":
                                return 0

                            if key.lower() == "e":
                                eject_menu()

                            if key.lower() == "a":
                                toggle_auto_eject()

                            if key.lower() == "c":
                                job_action_menu(
                                    "correct",
                                    old_settings,
                                )

                            if key.lower() == "d":
                                job_action_menu(
                                    "delete",
                                    old_settings,
                                )


                        continue

                    # -------------------------------------------------------
                    # CURRENT ACTIVITY
                    # -------------------------------------------------------

                    print()
                    print(
                        f"{BOLD}CURRENT ACTIVITY{RESET}"
                    )

                    print("-" * 120)

                    ripping = [
                        row
                        for row in jobs
                        if row["status"] == "RIPPING"
                    ]

                    transcoding = [
                        row
                        for row in jobs
                        if row["status"] == "TRANSCODING"
                    ]

                    selecting = [
                        row
                        for row in jobs
                        if row["status"] == "TITLE_SELECTED"
                    ]

                    processes = process_lines()

                    inspecting = [
                        line
                        for line in processes
                        if "makemkvcon" in line.lower()
                        and " info " in (
                            " " + line.lower() + " "
                        )
                    ]

                    if inspecting and not ripping and not transcoding:
                        print(
                            f"{YELLOW}● INSPECTING{RESET}    "
                            "disc identification / title scan"
                        )
                    else:
                        print(
                            f"{DIM}"
                            "○ INSPECTING    none"
                            f"{RESET}"
                        )

                    if ripping:
                        for row in ripping:
                            print(
                                f"{GREEN}● RIPPING{RESET}       "
                                f"{row['disc_label']} "
                                f"({row['drive']}) "
                                f"[{elapsed(row['rip_started_at'])}]"
                            )
                    else:
                        print(
                            f"{DIM}"
                            "○ RIPPING        none"
                            f"{RESET}"
                        )

                    if transcoding:
                        for row in transcoding:
                            print(
                                f"{GREEN}● TRANSCODING{RESET}  "
                                f"{row['disc_label']} "
                                f"({row['drive']}) "
                                f"[{elapsed(row['transcode_started_at'])}]"
                            )
                    else:
                        print(
                            f"{DIM}"
                            "○ TRANSCODING   none"
                            f"{RESET}"
                        )

                    if selecting:
                        for row in selecting:
                            print(
                                f"{YELLOW}● TITLE SELECT{RESET} "
                                f"{row['disc_label']} "
                                f"({row['drive']})"
                            )

                    # -------------------------------------------------------
                    # DRIVES
                    # -------------------------------------------------------

                    print()
                    print(
                        f"{BOLD}DRIVES{RESET}"
                    )

                    print("-" * 120)

                    for device in configured_drives():

                        drive_number = device[-1]

                        rows = drive_map[device]

                        current = next(
                            (
                                row
                                for row in rows
                                if row["status"]
                                not in ("LIBRARY_MOVED",)
                            ),
                            None,
                        )

                        media = drive_media_state(
                            device
                        )

                        if current:

                            print(
                                f"Drive {drive_number}: "
                                f"{GREEN}● "
                                f"{current['status']:<12}"
                                f"{RESET} "
                                f"{device:<10} "
                                f"{current['disc_label'] or 'UNKNOWN':<28}"
                            )

                        elif media == "MEDIA":

                            print(
                                f"Drive {drive_number}: "
                                f"{YELLOW}● DISC READY"
                                f"{RESET}   "
                                f"{device:<10} "
                                f"[{GREEN}E{RESET}] eject"
                            )

                        else:

                            print(
                                f"Drive {drive_number}: "
                                f"{DIM}○ EMPTY{RESET}        "
                                f"{device}"
                            )

                    # -------------------------------------------------------
                    # PIPELINE
                    # -------------------------------------------------------

                    print()
                    print(
                        f"{BOLD}PIPELINE{RESET}"
                    )

                    print("-" * 120)

                    active_statuses = {
                        "RIPPING",
                        "TRANSCODING",
                        "TITLE_SELECTED",
                        "MOVING",
                    }

                    drive0_active = any(
                        r["drive"] == "/dev/sr0"
                        and r["status"] in active_statuses
                        for r in jobs
                    )

                    drive1_active = any(
                        r["drive"] == "/dev/sr1"
                        and r["status"] in active_statuses
                        for r in jobs
                    )

                    print(
                        "Drive 0: "
                        + (
                            f"{GREEN}● ACTIVE{RESET}"
                            if drive0_active
                            else f"{DIM}○ IDLE{RESET}"
                        )
                    )

                    print(
                        "Drive 1: "
                        + (
                            f"{GREEN}● ACTIVE{RESET}"
                            if drive1_active
                            else f"{DIM}○ IDLE{RESET}"
                        )
                    )

                    print(
                        "Transcode: "
                        + (
                            f"{GREEN}● ACTIVE{RESET}"
                            if transcoding
                            else f"{DIM}○ IDLE{RESET}"
                        )
                    )

                    # -------------------------------------------------------
                    # LAST 5 JOBS
                    # -------------------------------------------------------

                    print()
                    print(
                        f"{BOLD}LAST 5 JOBS{RESET}"
                    )

                    print("-" * 120)

                    print(
                        f"{'ID':<4} "
                        f"{'DRIVE':<11} "
                        f"{'DISC':<28} "
                        f"{'STATUS':<22} "
                        f"{'AGE':<10}"
                    )

                    print("-" * 120)

                    for row in jobs:

                        if row["status"] == "RIPPING":
                            start = row["rip_started_at"]

                        elif row["status"] == "TRANSCODING":
                            start = row[
                                "transcode_started_at"
                            ]

                        else:
                            start = row[
                                "rip_started_at"
                            ]

                        label = (
                            row["disc_label"] or ""
                        )[:27]

                        print(
                            f"{row['id']:<4} "
                            f"{row['drive']:<11} "
                            f"{label:<28} "
                            f"{status_text(row['status']):<31} "
                            f"{elapsed(start):<10}"
                        )

                    # -------------------------------------------------------
                    # TV / MOVIE LIBRARY
                    # -------------------------------------------------------

                    print()
                    print(
                        f"{BOLD}TV LIBRARY{RESET}"
                    )

                    print("-" * 120)

                    tv_library, movie_library = library_status(conn)

                    if tv_library:
                        for (show, season), episodes in sorted(
                            tv_library.items(),
                            key=lambda item: (
                                item[0][0].lower(),
                                item[0][1],
                            ),
                        ):
                            # Put the status indicators before the season
                            # and episode text so the dashboard can be
                            # scanned visually from left to right.
                            parts = []

                            for episode in sorted(episodes):
                                indicator = episodes[episode]

                                if indicator == "✓":
                                    symbol = f"{GREEN}✓{RESET}"
                                elif indicator == "●":
                                    symbol = f"{YELLOW}●{RESET}"
                                elif indicator == "○":
                                    symbol = f"{DIM}○{RESET}"
                                else:
                                    symbol = f"{RED}✗{RESET}"

                                parts.append(
                                    f"{symbol} E{episode:02d}"
                                )

                            print(
                                f"{GREEN}✓{RESET} {show}"
                            )

                            if parts:
                                print(
                                    f"  "
                                    f"{GREEN}✓{RESET} S{season:02d}  "
                                    + "  ".join(parts)
                                )
                    else:
                        print(
                            f"{DIM}"
                            "No TV library entries yet."
                            f"{RESET}"
                        )

                    print()
                    print(
                        f"{BOLD}MOVIE LIBRARY{RESET}"
                    )

                    print("-" * 120)

                    if movie_library:
                        movie_name = (
                            movie_library["title"]
                            or movie_library["disc_label"]
                            or "UNKNOWN MOVIE"
                        )

                        movie_status = movie_library["status"]

                        if movie_status == "LIBRARY_MOVED":
                            print(
                                f"{GREEN}✓{RESET} "
                                f"{movie_name}"
                            )

                        elif movie_status in (
                            "RIPPING",
                            "TRANSCODING",
                        ):
                            print(
                                f"{YELLOW}●{RESET} "
                                f"{movie_name}"
                            )

                        elif movie_status in (
                            "RIP_FAILED",
                            "TRANSCODE_FAILED",
                        ):
                            print(
                                f"{RED}✗{RESET} "
                                f"{movie_name}"
                            )

                        else:
                            print(
                                f"{YELLOW}●{RESET} "
                                f"{movie_name}"
                            )
                    else:
                        print(
                            f"{DIM}"
                            "No recent movie processing."
                            f"{RESET}"
                        )

                    # -------------------------------------------------------
                    # ACTIVE PROCESSES
                    # -------------------------------------------------------


                    print()
                    print(
                        f"{BOLD}ACTIVE PROCESSES{RESET}"
                    )

                    print("-" * 120)

                    makemkv, handbrake, processes = (
                        process_summary()
                    )

                    if makemkv:
                        for line in makemkv:
                            print(line[:119])

                    if handbrake:
                        for line in handbrake:
                            print(line[:119])

                    if not makemkv and not handbrake:
                        print(
                            f"{DIM}"
                            "No MakeMKV or HandBrake process "
                            "currently detected."
                            f"{RESET}"
                        )

                    # -------------------------------------------------------
                    # RIP OUTPUT
                    # -------------------------------------------------------

                    print()
                    print(
                        f"{BOLD}RIP OUTPUT{RESET}"
                    )

                    print("-" * 120)

                    if os.path.isdir(RIP_ROOT):

                        entries = sorted(
                            os.scandir(RIP_ROOT),
                            key=lambda entry: entry.name,
                        )

                        found = False

                        for entry in entries:

                            if not entry.is_dir():
                                continue

                            found = True

                            size = directory_size(
                                entry.path
                            )

                            old = previous_sizes.get(
                                entry.name
                            )

                            growth = ""

                            if old is not None:

                                delta = size - old

                                if delta > 0:
                                    growth = (
                                        f"  {GREEN}+"
                                        f"{bytes_text(delta)}"
                                        f" since last refresh"
                                        f"{RESET}"
                                    )

                                elif delta == 0:
                                    growth = (
                                        f"  {YELLOW}"
                                        "no growth since last refresh"
                                        f"{RESET}"
                                    )

                            previous_sizes[
                                entry.name
                            ] = size

                            print(
                                f"{entry.name:<30}"
                                f"{bytes_text(size):>12}"
                                f"{growth}"
                            )

                        if not found:
                            print(
                                f"{DIM}"
                                "No active rip directories."
                                f"{RESET}"
                            )

                    else:
                        print(
                            f"{DIM}"
                            f"{RIP_ROOT} does not exist."
                            f"{RESET}"
                        )

                    # -------------------------------------------------------
                    # RECENT ARM ACTIVITY
                    # -------------------------------------------------------

                    print()
                    print(
                        f"{BOLD}RECENT ARM ACTIVITY{RESET}"
                    )

                    print("-" * 120)

                    events = conn.execute("""
                        SELECT
                            timestamp,
                            level,
                            event,
                            message
                        FROM events
                        ORDER BY timestamp DESC
                        LIMIT 8
                    """).fetchall()

                    for event in reversed(events):

                        timestamp = (
                            event["timestamp"]
                            or ""
                        )

                        timestamp = (
                            timestamp
                            .replace("T", " ")
                            [:19]
                        )

                        message = (
                            event["message"]
                            or ""
                        ).replace(
                            "\n",
                            " ",
                        )

                        print(
                            f"{timestamp}  "
                            f"{event['level']:<5} "
                            f"{event['event']:<28} "
                            f"{message[:55]}"
                        )

            except Exception as exc:

                print()
                print(
                    f"{RED}"
                    f"DATABASE ERROR: {exc}"
                    f"{RESET}"
                )

            # ---------------------------------------------------------------
            # SYSTEM
            # ---------------------------------------------------------------

            print()
            print(
                f"{BOLD}SYSTEM{RESET}"
            )

            print("-" * 120)

            load = run([
                "awk",
                "{print $1}",
                "/proc/loadavg",
            ])

            memory = run([
                "bash",
                "-c",
                "free -h | "
                "awk '/^Mem:/ "
                "{print $3 \" / \" $2}'",
            ])

            disk = run([
                "bash",
                "-c",
                "df -h /rips | "
                "awk 'NR==2 "
                "{print $3 \" / \" $2 \" (\" $5 \")\"}'",
            ])

            print(
                f"Load: {load or '-':<10}"
                f"RAM: {memory or '-':<20}"
                f"/rips: {disk or '-'}"
            )

            # ---------------------------------------------------------------
            # KEYBOARD CONTROLS
            # ---------------------------------------------------------------

            print()
            print(
                f"{BOLD}"
                "[E] Eject    [A] Auto-eject    "
                "[C] Correct    [D] Delete    [Q] Quit"
                f"{RESET}"
            )

            print(
                f"{DIM}"
                "Press a key at any time • "
                "Refreshing every 2 seconds"
                f"{RESET}"
            )

            # Wait for one key, but refresh automatically after 2 seconds.
            key = read_key(timeout=2)

            if key is None:
                continue

            key = key.lower()

            if key == "q":
                clear()
                print(
                    "ARM status dashboard closed."
                )
                break

            if key == "e":
                eject_menu()

            if key == "a":
                toggle_auto_eject()

            if key == "c":
                job_action_menu(
                    "correct",
                    old_settings,
                )

            if key == "d":
                job_action_menu(
                    "delete",
                    old_settings,
                )

    finally:
        # Always restore the user's terminal settings.
        termios.tcsetattr(
            sys.stdin,
            termios.TCSADRAIN,
            old_settings,
        )

        clear()


if __name__ == "__main__":
    main()
