```python
"""planner.py - greedy selection of fictional campaign proposals."""

from typing import Any, Iterable, Mapping


def _is_int(value: Any) -> bool:
    return isinstance(value, int) and not isinstance(value, bool)


def plan(rows: Iterable[Mapping[str, Any]], budget_cents: int, excluded_channels=()):
    """Select rows in input order while they fit in the remaining budget.

    Each row has a string "id" and a nonnegative integer "cost_cents".
    Only the first occurrence of each id is considered; later rows with the
    same id are ignored, even if the first occurrence is archived or too
    expensive. Rows with a true "archived" value are excluded (missing
    archived means false).

    A row is selected if its cost is at most the remaining budget; otherwise
    it is skipped and later rows are still considered.

    excluded_channels is reserved for a later feature and currently has no
    effect. Neither rows nor excluded_channels are mutated.

    Returns a dict with selected_ids, spent_cents and remaining_cents.
    Raises ValueError for negative or non-integer (including bool) budgets.
    """
    if not _is_int(budget_cents):
        raise ValueError("budget_cents must be an integer")
    if budget_cents < 0:
        raise ValueError("budget_cents must be nonnegative")

    remaining = budget_cents
    spent = 0
    selected_ids = []
    seen_ids = set()

    for row in rows:
        row_id = row["id"]
        if row_id in seen_ids:
            continue
        seen_ids.add(row_id)

        if row.get("archived", False):
            continue

        cost = row["cost_cents"]
        if cost <= remaining:
            selected_ids.append(row_id)
            remaining -= cost
            spent += cost

    return {
        "selected_ids": selected_ids,
        "spent_cents": spent,
        "remaining_cents": remaining,
    }
```