# SPDX-License-Identifier: AGPL-3.0-only
# Copyright (c) 2026 sol pbc

"""Clock-aligned task scheduler for the supervisor.

Reads schedule definitions from config/schedules.json and submits tasks
via Callosum at minute, hour, and day boundaries. Last-run state is read from
health/scheduler.json, written by supervisor completion writeback, to avoid
re-firing across restarts.

Runtime functions (init, check) are used by the supervisor.
The main() function provides the ``journal schedule`` CLI.
"""

from __future__ import annotations

import argparse
import json
import logging
import re
from datetime import datetime, timedelta
from pathlib import Path
from typing import Any

from solstone.think.schedule_config import (
    RESERVED_METADATA_KEYS,
    read_schedules,
    set_schedule_entries,
)
from solstone.think.utils import (
    get_journal,
    now_ms,
    parse_duration_seconds,
    require_solstone,
    setup_cli,
)

logger = logging.getLogger(__name__)

# Valid schedule intervals
INTERVALS = {"hourly", "daily", "weekly"}

# ---------------------------------------------------------------------------
# Module state (populated by init(), used by check())
# ---------------------------------------------------------------------------
_entries: dict[str, dict[str, Any]] = {}
_state: dict[str, dict[str, Any]] = {}
_callosum: Any = None  # CallosumConnection
_last_minute: datetime | None = None
_last_hour: datetime | None = None
_daily_time: str | None = None
_last_daily_mark: datetime | None = None
_weekly_day: str | None = None
_weekly_time: str | None = None
_last_weekly_mark: datetime | None = None
# Coarse entries whose boundary emit failed; retried on later minute ticks.
_pending_retry: set[str] = set()


# ---------------------------------------------------------------------------
# Config + state I/O
# ---------------------------------------------------------------------------


def load_config() -> dict[str, dict[str, Any]]:
    """Read config/schedules.json and return validated entries."""
    global _daily_time, _weekly_day, _weekly_time

    config_path = Path(get_journal()) / "config" / "schedules.json"
    if not config_path.exists():
        _daily_time = None
        _weekly_day = None
        _weekly_time = None
        return {}

    try:
        with open(config_path, "r", encoding="utf-8") as f:
            raw = json.load(f)
    except (json.JSONDecodeError, OSError) as exc:
        logger.warning("Failed to load schedules config: %s", exc)
        _daily_time = None
        _weekly_day = None
        _weekly_time = None
        return {}

    if not isinstance(raw, dict):
        logger.warning(
            "schedules.json must be a JSON object, got %s", type(raw).__name__
        )
        _daily_time = None
        _weekly_day = None
        _weekly_time = None
        return {}

    # Extract daily_time metadata (not a schedule entry)
    _daily_time = raw.get("daily_time", None)
    if _daily_time is not None and not isinstance(_daily_time, str):
        logger.warning("schedules.json: daily_time must be a string, ignoring")
        _daily_time = None

    # Extract weekly_day metadata
    _weekly_day = raw.get("weekly_day", None)
    if _weekly_day is not None and not isinstance(_weekly_day, str):
        logger.warning("schedules.json: weekly_day must be a string, ignoring")
        _weekly_day = None
    elif _weekly_day is not None and _parse_weekly_day(_weekly_day) is None:
        logger.warning(
            "schedules.json: unrecognized weekly_day '%s', ignoring", _weekly_day
        )
        _weekly_day = None

    # Extract weekly_time metadata
    _weekly_time = raw.get("weekly_time", None)
    if _weekly_time is not None and not isinstance(_weekly_time, str):
        logger.warning("schedules.json: weekly_time must be a string, ignoring")
        _weekly_time = None

    entries: dict[str, dict[str, Any]] = {}
    for name, entry in raw.items():
        if name in RESERVED_METADATA_KEYS:
            continue

        if not isinstance(entry, dict):
            logger.warning("Schedule '%s': expected object, skipping", name)
            continue

        cmd = entry.get("cmd")
        if not cmd or not isinstance(cmd, list):
            logger.warning("Schedule '%s': missing or invalid 'cmd', skipping", name)
            continue

        every = entry.get("every")
        if every in INTERVALS or _is_minute_interval(every):
            if _is_minute_interval(every):
                match = re.fullmatch(r"(\d+)m", every)
                if match and int(match.group(1)) < 5:
                    logger.warning(
                        "Schedule '%s': interval '%s' is below the 5m floor; "
                        "clamped to 5m",
                        name,
                        every,
                    )
        else:
            logger.warning(
                "Schedule '%s': unknown interval '%s' (expected %s), skipping",
                name,
                every,
                "/".join(sorted(INTERVALS)) + " or Nm",
            )
            continue

        if not entry.get("enabled", True):
            continue

        validated = {"cmd": cmd, "every": every}
        max_runtime = entry.get("max_runtime")
        if max_runtime is not None:
            # D-C / design §2: preserve caps for TaskQueue registration,
            # not as extra supervisor.request payload fields.
            try:
                validated["max_runtime"] = parse_duration_seconds(max_runtime)
            except ValueError:
                logger.warning(
                    "Schedule '%s': invalid max_runtime %r, dropping cap",
                    name,
                    max_runtime,
                )

        entries[name] = validated

    return entries


def load_state() -> dict[str, dict[str, Any]]:
    """Read health/scheduler.json."""
    state_path = Path(get_journal()) / "health" / "scheduler.json"
    if not state_path.exists():
        return {}

    try:
        with open(state_path, "r", encoding="utf-8") as f:
            return json.load(f)
    except (json.JSONDecodeError, OSError) as exc:
        logger.warning("Failed to load scheduler state: %s", exc)
        return {}


# ---------------------------------------------------------------------------
# Boundary helpers
# ---------------------------------------------------------------------------


def _hour_mark(dt: datetime) -> datetime:
    """Truncate datetime to the start of its hour."""
    return dt.replace(minute=0, second=0, microsecond=0)


def _parse_daily_time(raw: str | None) -> tuple[int, int] | None:
    """Parse HH:MM daily time string. Returns (hour, minute) or None."""
    if not raw or not isinstance(raw, str):
        return None
    parts = raw.split(":")
    if len(parts) != 2:
        return None
    try:
        h, m = int(parts[0]), int(parts[1])
        if 0 <= h <= 23 and 0 <= m <= 59:
            return (h, m)
    except ValueError:
        return None
    return None


DAY_NAMES: dict[str, int] = {
    "monday": 0,
    "mon": 0,
    "tuesday": 1,
    "tue": 1,
    "wednesday": 2,
    "wed": 2,
    "thursday": 3,
    "thu": 3,
    "friday": 4,
    "fri": 4,
    "saturday": 5,
    "sat": 5,
    "sunday": 6,
    "sun": 6,
}


def _parse_weekly_day(raw: str | None) -> int | None:
    """Parse day-of-week name. Returns weekday int (0=Monday, 6=Sunday) or None."""
    if not raw or not isinstance(raw, str):
        return None
    return DAY_NAMES.get(raw.strip().lower())


def _compute_daily_mark(now: datetime, daily_time_str: str | None) -> datetime:
    """Compute the most recent daily boundary datetime.

    With a configured daily_time (e.g. "03:00"), the boundary is that time
    today if already passed, otherwise that time yesterday. Without a
    configured time, falls back to midnight (start of today).
    """
    parsed = _parse_daily_time(daily_time_str)
    if parsed is None:
        return now.replace(hour=0, minute=0, second=0, microsecond=0)
    h, m = parsed
    today_mark = now.replace(hour=h, minute=m, second=0, microsecond=0)
    if now >= today_mark:
        return today_mark
    return today_mark - timedelta(days=1)


def _compute_weekly_mark(
    now: datetime, weekly_day: int, weekly_time_str: str | None
) -> datetime:
    """Compute the most recent weekly boundary datetime.

    Returns the most recent occurrence of the target weekday at the target time.
    If now is past this week's boundary, returns this week's. Otherwise last week's.
    """
    parsed = _parse_daily_time(weekly_time_str)
    if parsed is None:
        h, m = 3, 0  # default 03:00
    else:
        h, m = parsed
    days_since = (now.weekday() - weekly_day) % 7
    target_date = now - timedelta(days=days_since)
    target_mark = target_date.replace(hour=h, minute=m, second=0, microsecond=0)
    if now >= target_mark:
        return target_mark
    return target_mark - timedelta(weeks=1)


def _is_minute_interval(every: Any) -> bool:
    return isinstance(every, str) and re.fullmatch(r"\d+m", every) is not None


def _parse_minute_interval(every: Any) -> int | None:
    if not isinstance(every, str):
        return None
    match = re.fullmatch(r"(\d+)m", every)
    if not match:
        return None
    return max(int(match.group(1)), 5)


def _effective_interval(every: Any) -> Any:
    """Return the interval as it will actually run, for honest display.

    Sub-5m minute intervals run at the 5m floor (see _parse_minute_interval);
    this surfaces that. Non-minute or >=5m values pass through unchanged.
    """
    if _is_minute_interval(every):
        return f"{_parse_minute_interval(every)}m"
    return every


def _is_due(entry: dict, state_entry: dict | None, now: datetime) -> bool:
    """Check if an entry is due based on its interval and last_run."""
    last_run = (state_entry or {}).get("last_run")
    if last_run is None:
        return True

    try:
        last_dt = datetime.fromtimestamp(last_run)
    except (OSError, ValueError):
        return True

    every = entry["every"]
    if every == "hourly":
        return last_dt < _hour_mark(now)
    if every == "daily":
        return last_dt < _compute_daily_mark(now, _daily_time)
    if every == "weekly":
        weekly_day_val = _parse_weekly_day(_weekly_day)
        if weekly_day_val is None:
            weekly_day_val = 6  # default Sunday
        return last_dt < _compute_weekly_mark(now, weekly_day_val, _weekly_time)
    if _is_minute_interval(every):
        minutes = _parse_minute_interval(every)
        if minutes is None:
            return False
        return last_dt <= now - timedelta(minutes=minutes)
    return False


# ---------------------------------------------------------------------------
# Runtime API (called by supervisor)
# ---------------------------------------------------------------------------


def init(callosum: Any) -> None:
    """Initialize scheduler with a Callosum connection. Load config and state."""
    global _entries, _state, _callosum, _last_minute, _last_hour
    global _last_daily_mark, _last_weekly_mark, _pending_retry

    _callosum = callosum
    _entries = load_config()
    _state = load_state()
    _pending_retry = set()

    now = datetime.now()
    _last_minute = now.replace(second=0, microsecond=0)
    _last_hour = _hour_mark(now)
    _last_daily_mark = _compute_daily_mark(now, _daily_time)
    weekly_day_val = _parse_weekly_day(_weekly_day)
    if weekly_day_val is None:
        weekly_day_val = 6
    _last_weekly_mark = _compute_weekly_mark(now, weekly_day_val, _weekly_time)

    if _entries:
        logger.info(
            "Scheduler initialized with %d schedule(s): %s",
            len(_entries),
            ", ".join(sorted(_entries)),
        )
    else:
        logger.info("Scheduler initialized (no schedules configured)")


def collect_runtime_caps() -> list[tuple[list[str], int]]:
    """Return configured task runtime caps from loaded schedule entries."""
    caps: list[tuple[list[str], int]] = []
    for entry in _entries.values():
        max_runtime = entry.get("max_runtime")
        if max_runtime is not None:
            caps.append((list(entry["cmd"]), max_runtime))
    return caps


def register_defaults() -> None:
    """Ensure built-in default schedules exist in the config file.

    Called by the supervisor after init(). Writes missing defaults to
    config/schedules.json and reloads entries.
    """
    global _entries

    need_heartbeat = "heartbeat" not in _entries
    need_weekly = "weekly-agents" not in _entries
    need_cadence = "cadence" not in _entries
    need_brain = "brain" not in _entries
    need_facet_candidates = "facet-candidates" not in _entries
    need_rebuild_edges = "rebuild-edges" not in _entries

    if (
        not need_heartbeat
        and not need_weekly
        and not need_cadence
        and not need_brain
        and not need_facet_candidates
        and not need_rebuild_edges
    ):
        return

    raw = read_schedules()
    additions: dict[str, dict[str, Any]] = {}

    if need_heartbeat and "heartbeat" not in raw:
        additions["heartbeat"] = {
            "cmd": ["journal", "heartbeat"],
            "every": "daily",
            "enabled": True,
            "max_runtime": "10m",
        }

    if need_weekly and "weekly-agents" not in raw:
        additions["weekly-agents"] = {
            "cmd": ["journal", "think", "--weekly", "-v"],
            "every": "weekly",
            "enabled": True,
            "max_runtime": "30m",
        }

    if need_cadence and "cadence" not in raw:
        additions["cadence"] = {
            "cmd": ["journal", "think", "--cadence"],
            "every": "5m",
            "enabled": True,
            "max_runtime": "10m",
        }

    if need_brain and "brain" not in raw:
        additions["brain"] = {
            "cmd": ["journal", "brain", "refresh"],
            "every": "daily",
            "enabled": True,
            "max_runtime": "5m",
        }

    if need_facet_candidates and "facet-candidates" not in raw:
        additions["facet-candidates"] = {
            "cmd": ["journal", "facet-candidates"],
            "every": "weekly",
            "enabled": True,
            "max_runtime": "10m",
        }

    if need_rebuild_edges and "rebuild-edges" not in raw:
        additions["rebuild-edges"] = {
            "cmd": ["journal", "indexer", "--rebuild-edges"],
            "every": "weekly",
            "enabled": True,
            "max_runtime": "10m",
        }

    if not additions:
        return

    set_schedule_entries(additions)
    logger.info("Auto-registered default schedule(s) in config/schedules.json")

    # Reload to pick up the new entry
    _entries = load_config()


def _submit_entry(name: str, entry: dict) -> bool:
    """Emit a supervisor.request for one schedule entry.

    Returns True iff the emit succeeded. Shared by check() (edge-triggered)
    and catch_up() (level-triggered). Callers own the due/boundary checks;
    this does emission only.
    """
    ref = f"sched:{name}:{now_ms()}"
    cmd = entry["cmd"]

    if not _callosum:
        logger.warning("No callosum connection for scheduled task: %s", name)
        return False

    ok = _callosum.emit(
        "supervisor",
        "request",
        cmd=cmd,
        ref=ref,
        scheduler_name=name,
    )
    if ok:
        logger.info(
            "Scheduled task submitted: %s → %s (ref=%s)",
            name,
            " ".join(cmd),
            ref,
        )
        return True

    logger.warning("Failed to emit scheduled task %s (callosum not connected)", name)
    return False


def check() -> None:
    """Check for clock boundaries and submit due tasks.

    Called each supervisor tick (~1s). Does nothing unless a clock
    boundary has been crossed since the last check.
    """
    global _entries, _state, _last_minute, _last_hour, _last_daily_mark
    global _last_weekly_mark, _pending_retry

    if _last_hour is None:
        return

    now = datetime.now()
    current_minute = now.replace(second=0, microsecond=0)
    current_hour = _hour_mark(now)
    current_daily_mark = _compute_daily_mark(now, _daily_time)
    weekly_day_val = _parse_weekly_day(_weekly_day)
    if weekly_day_val is None:
        weekly_day_val = 6
    current_weekly_mark = _compute_weekly_mark(now, weekly_day_val, _weekly_time)

    hour_changed = current_hour != _last_hour
    daily_mark_changed = current_daily_mark != _last_daily_mark
    weekly_mark_changed = current_weekly_mark != _last_weekly_mark
    minute_changed = current_minute != _last_minute

    if (
        not hour_changed
        and not daily_mark_changed
        and not weekly_mark_changed
        and not minute_changed
    ):
        return

    # Clock boundary crossed — reload config on coarse boundaries only.
    if hour_changed or daily_mark_changed or weekly_mark_changed:
        _entries = load_config()
    _state = load_state()
    _last_minute = current_minute
    _last_hour = current_hour
    # Recompute with potentially updated _daily_time from config reload
    new_daily_mark = _compute_daily_mark(now, _daily_time)
    if new_daily_mark != _last_daily_mark:
        daily_mark_changed = True
    _last_daily_mark = new_daily_mark
    new_weekly_day_val = _parse_weekly_day(_weekly_day)
    if new_weekly_day_val is None:
        new_weekly_day_val = 6
    new_weekly_mark = _compute_weekly_mark(now, new_weekly_day_val, _weekly_time)
    if new_weekly_mark != _last_weekly_mark:
        weekly_mark_changed = True
    _last_weekly_mark = new_weekly_mark

    # Retry pass: re-attempt coarse entries whose boundary emit previously
    # failed. Bypasses the boundary gating below so a lost coarse cycle
    # recovers within minutes instead of at the next boundary. The set is
    # seeded ONLY by emit failure (boundary pass, below) — never by is-due —
    # so a successful emit is never repeated. Drop names dropped from _entries
    # (reconciled at the next coarse config reload), no longer due (supervisor
    # writeback landed), or that now succeed.
    attempted: set[str] = set()
    for name in list(_pending_retry):
        entry = _entries.get(name)
        if entry is None or not _is_due(entry, _state.get(name), now):
            _pending_retry.discard(name)
            continue
        attempted.add(name)
        if _submit_entry(name, entry):
            _pending_retry.discard(name)

    if not _entries:
        return

    submitted = False
    for name, entry in _entries.items():
        if name in attempted:
            continue
        every = entry["every"]

        # Only check entries matching the boundary that changed
        if every == "hourly" and not hour_changed:
            continue
        if every == "daily" and not daily_mark_changed:
            continue
        if every == "weekly" and not weekly_mark_changed:
            continue
        if _is_minute_interval(every) and not minute_changed:
            continue

        if not _is_due(entry, _state.get(name), now):
            continue

        if _submit_entry(name, entry):
            submitted = True
        elif not _is_minute_interval(every):
            # Coarse emit failed at its boundary — retry on later minute
            # ticks. Minute intervals self-heal via the next minute tick +
            # _is_due's sliding window, so they do NOT join the retry set.
            _pending_retry.add(name)

    if submitted:
        logger.debug("Submitted scheduled task batch")


def catch_up() -> None:
    """Submit overdue schedule entries on startup (level-triggered).

    Complements the edge-triggered check(): init() re-baselines the boundary
    marks to now on every (re)start, so an entry whose boundary passed while
    the supervisor was down would never fire via check(). This pass submits
    each entry that _is_due() reports overdue exactly once, with no live
    boundary transition required. Relies on _is_due() plus the completion
    writeback (health/scheduler.json) to avoid re-firing a completed task on a
    subsequent restart. Does not touch the boundary marks — check() continues
    normally afterward.
    """
    if not _entries:
        return

    now = datetime.now()
    caught_up: list[str] = []
    for name, entry in _entries.items():
        if not _is_due(entry, _state.get(name), now):
            continue
        if _submit_entry(name, entry):
            caught_up.append(name)

    if caught_up:
        logger.info(
            "Startup catch-up submitted %d overdue schedule(s): %s",
            len(caught_up),
            ", ".join(sorted(caught_up)),
        )


def collect_status() -> list[dict[str, Any]]:
    """Return schedule status for supervisor.status events."""
    now = datetime.now()
    result = []
    for name, entry in _entries.items():
        state_entry = _state.get(name)
        last_run = (state_entry or {}).get("last_run")
        entry_status = {
            "name": name,
            "every": _effective_interval(entry["every"]),
            "last_run": last_run,
            "due": _is_due(entry, state_entry, now),
        }
        entry_status["next_run"] = _compute_next_run(entry, state_entry, now)
        if entry["every"] == "daily" and _daily_time:
            entry_status["daily_time"] = _daily_time
        if entry["every"] == "weekly":
            if _weekly_day:
                entry_status["weekly_day"] = _weekly_day
            if _weekly_time:
                entry_status["weekly_time"] = _weekly_time
        result.append(entry_status)
    return result


def _compute_next_run(entry: dict, state_entry: dict | None, now: datetime) -> int:
    """Compute next run time as epoch milliseconds."""
    every = entry["every"]
    if every == "hourly":
        mark = _hour_mark(now)
        nxt = mark if _is_due(entry, state_entry, now) else mark + timedelta(hours=1)
    elif every == "daily":
        mark = _compute_daily_mark(now, _daily_time)
        nxt = mark if _is_due(entry, state_entry, now) else mark + timedelta(days=1)
    elif every == "weekly":
        weekly_day_val = _parse_weekly_day(_weekly_day)
        if weekly_day_val is None:
            weekly_day_val = 6
        mark = _compute_weekly_mark(now, weekly_day_val, _weekly_time)
        nxt = mark if _is_due(entry, state_entry, now) else mark + timedelta(weeks=1)
    elif _is_minute_interval(every):
        minutes = _parse_minute_interval(every) or 5
        if _is_due(entry, state_entry, now):
            nxt = now
        else:
            last_run = (state_entry or {}).get("last_run")
            nxt = datetime.fromtimestamp(last_run) + timedelta(minutes=minutes)
    else:
        return int(now.timestamp() * 1000)
    return int(nxt.timestamp() * 1000)


# ---------------------------------------------------------------------------
# CLI: journal schedule
# ---------------------------------------------------------------------------


def _format_timestamp(epoch: float | None) -> str:
    """Format an epoch timestamp for display."""
    if epoch is None:
        return "never"
    try:
        return datetime.fromtimestamp(epoch).strftime("%Y-%m-%d %H:%M")
    except (OSError, ValueError):
        return "invalid"


def _format_next_due(entry: dict, state_entry: dict | None, now: datetime) -> str:
    """Format the next due time for display."""
    if _is_due(entry, state_entry, now):
        return "now"

    every = entry["every"]
    if every == "hourly":
        nxt = _hour_mark(now) + timedelta(hours=1)
        return nxt.strftime("%H:%M")
    if every == "daily":
        parsed = _parse_daily_time(_daily_time)
        return f"{parsed[0]:02d}:{parsed[1]:02d}" if parsed else "midnight"
    if every == "weekly":
        weekly_day_val = _parse_weekly_day(_weekly_day)
        if weekly_day_val is None:
            weekly_day_val = 6
        weekly_mark = _compute_weekly_mark(now, weekly_day_val, _weekly_time)
        nxt = weekly_mark + timedelta(weeks=1)
        return f"{nxt.strftime('%A')} {nxt.strftime('%H:%M')}"
    if _is_minute_interval(every):
        minutes = _parse_minute_interval(every) or 5
        last_run = (state_entry or {}).get("last_run")
        nxt = datetime.fromtimestamp(last_run) + timedelta(minutes=minutes)
        return nxt.strftime("%H:%M")
    return "?"


def main() -> None:
    """CLI entry point for journal schedule."""
    parser = argparse.ArgumentParser(description="Show scheduled tasks")
    setup_cli(parser)
    require_solstone()

    journal = Path(get_journal())
    config_path = journal / "config" / "schedules.json"
    state_path = journal / "health" / "scheduler.json"

    # Load config (all entries, including disabled for display)
    config: dict[str, Any] = {}
    if config_path.exists():
        try:
            with open(config_path, "r", encoding="utf-8") as f:
                config = json.load(f)
        except (json.JSONDecodeError, OSError) as exc:
            print(f"Error reading {config_path}: {exc}")
            return

    # Extract scheduling metadata before processing entries.
    global _daily_time, _weekly_day, _weekly_time
    raw_daily_time = config.get("daily_time", None)
    _daily_time = raw_daily_time if isinstance(raw_daily_time, str) else None
    raw_weekly_day = config.get("weekly_day", None)
    raw_weekly_time = config.get("weekly_time", None)
    _weekly_day = raw_weekly_day if isinstance(raw_weekly_day, str) else None
    _weekly_time = raw_weekly_time if isinstance(raw_weekly_time, str) else None
    config = {
        name: entry
        for name, entry in config.items()
        if name not in RESERVED_METADATA_KEYS
    }

    if not config:
        print("No schedules configured.")
        print(f"\nAdd schedules to: {config_path}")
        return

    # Load state
    state: dict[str, Any] = {}
    if state_path.exists():
        try:
            with open(state_path, "r", encoding="utf-8") as f:
                state = json.load(f)
        except (json.JSONDecodeError, OSError):
            pass

    now = datetime.now()

    # Compute column widths
    names = list(config.keys())
    name_width = max(max(len(n) for n in names), 4)
    every_width = 8
    last_run_width = 18
    next_due_width = 10

    # Header
    header = (
        f"  {'NAME':<{name_width}}  {'EVERY':<{every_width}}  "
        f"{'LAST RUN':<{last_run_width}}  {'NEXT DUE':<{next_due_width}}  CMD"
    )
    print(header)
    print()

    for name, raw_entry in sorted(config.items()):
        if not isinstance(raw_entry, dict):
            continue

        every = _effective_interval(raw_entry.get("every", "?"))
        cmd = raw_entry.get("cmd", [])
        enabled = raw_entry.get("enabled", True)
        state_entry = state.get(name)

        last_run_str = _format_timestamp((state_entry or {}).get("last_run"))

        # Build a validated entry for _is_due / _format_next_due
        if (every in INTERVALS or _is_minute_interval(every)) and enabled:
            entry = {"cmd": cmd, "every": every}
            next_due_str = _format_next_due(entry, state_entry, now)
        else:
            next_due_str = "disabled" if not enabled else "?"

        cmd_str = " ".join(cmd) if isinstance(cmd, list) else str(cmd)

        tags = ""
        if not enabled:
            tags = " [disabled]"

        line = (
            f"  {name:<{name_width}}  {every:<{every_width}}  "
            f"{last_run_str:<{last_run_width}}  {next_due_str:<{next_due_width}}  {cmd_str}{tags}"
        )
        print(line.rstrip())

    print()
    print(f"Config: {config_path}")
