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


def _is_valid_int(value):
    return isinstance(value, int) and not isinstance(value, bool)


def _normalize_channel(value):
    return value.strip().casefold()


def plan(rows, budget_cents, excluded_channels=()):
    """Select rows in input order while they fit in the remaining budget.

    Each row is a mapping with a string "id" and a nonnegative integer
    "cost_cents". Only the first occurrence of each id is considered;
    later rows with an already-seen id are ignored, even if the first
    occurrence was archived, excluded by channel or too expensive.
    After deduplication, rows are skipped when:
      - "archived" is true (missing "archived" means false), or
      - the row's "channel" matches an entry in excluded_channels after
        normalizing both with strip().casefold() (a missing channel is
        treated as an empty string).
    A remaining 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 a list or tuple of strings; an empty one
    excludes nothing.

    Returns a dict with selected_ids, spent_cents and remaining_cents.
    Neither rows nor excluded_channels are mutated.
    """
    if not _is_valid_int(budget_cents):
        raise ValueError("budget_cents must be an integer")
    if budget_cents < 0:
        raise ValueError("budget_cents must be nonnegative")

    excluded = {_normalize_channel(ch) for ch in excluded_channels}

    remaining = budget_cents
    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

        if _normalize_channel(row.get("channel", "")) in excluded:
            continue

        cost = row["cost_cents"]
        if not _is_valid_int(cost) or cost < 0:
            raise ValueError("cost_cents must be a nonnegative integer")
        if cost <= remaining:
            selected_ids.append(row_id)
            remaining -= cost

    return {
        "selected_ids": selected_ids,
        "spent_cents": budget_cents - remaining,
        "remaining_cents": remaining,
    }
```