```python
"""Fictional campaign proposal planner."""

from decimal import Decimal, InvalidOperation, ROUND_HALF_UP, localcontext

__all__ = ["plan"]


def _row_cost_cents(row):
    if "price" in row:
        price = row["price"]
        if not isinstance(price, str):
            raise ValueError("price must be a decimal string")
        text = price.strip()
        if not text:
            raise ValueError("price must be a decimal string")
        try:
            value = Decimal(text)
        except InvalidOperation:
            raise ValueError("price must be a valid decimal string") from None
        if not value.is_finite():
            raise ValueError("price must be finite")
        if value < 0:
            raise ValueError("price must be nonnegative")
        with localcontext() as context:
            digits = len(value.as_tuple().digits)
            exponent = value.as_tuple().exponent
            adjusted = value.adjusted() if value != 0 else 0
            context.prec = max(28, digits + abs(exponent) + 10, adjusted + 15)
            cents = (value * Decimal(100)).to_integral_value(
                rounding=ROUND_HALF_UP
            )
        return int(cents)

    if "cost_cents" not in row:
        raise ValueError("cost_cents is required when price is absent")
    cost_cents = row["cost_cents"]
    if isinstance(cost_cents, bool) or not isinstance(cost_cents, int):
        raise ValueError("cost_cents must be an integer")
    if cost_cents < 0:
        raise ValueError("cost_cents must be nonnegative")
    return cost_cents


def plan(rows, budget_cents, excluded_channels=()):
    """Select proposals by descending priority within the given budget.

    Args:
        rows: Iterable of mappings with ``id`` and ``cost_cents``, and
            optionally ``archived``, ``channel``, ``priority``, and
            ``price``. For each ``id``, only its first occurrence is
            considered. A present ``price`` is used instead of
            ``cost_cents``.
        budget_cents: Nonnegative integer budget in cents.
        excluded_channels: A list or tuple of channel names to exclude.
            Channel matching is normalized with ``strip().casefold()``.

    Returns:
        A dictionary with exactly ``selected_ids``, ``spent_cents``,
        ``remaining_cents``, and ``skipped_ids``.

    Raises:
        ValueError: If ``budget_cents`` is negative, is not an integer,
            or is a boolean; if an eligible row has a malformed,
            negative, non-finite, or non-string ``price``; or if an
            eligible legacy row has a missing, negative, or non-integer
            ``cost_cents``, including booleans.
    """
    if isinstance(budget_cents, bool) or not isinstance(budget_cents, int):
        raise ValueError("budget_cents must be an integer")
    if budget_cents < 0:
        raise ValueError("budget_cents must be nonnegative")

    normalized_excluded_channels = {
        channel.strip().casefold() for channel in excluded_channels
    }

    seen_ids = set()
    candidates = []

    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

        channel = row.get("channel", "")
        if channel.strip().casefold() in normalized_excluded_channels:
            continue

        candidates.append(
            (row_id, _row_cost_cents(row), row.get("priority", 0))
        )

    candidates.sort(key=lambda candidate: -candidate[2])

    selected_ids = []
    skipped_ids = []
    spent_cents = 0
    remaining_cents = budget_cents

    for row_id, cost_cents, _priority in candidates:
        if cost_cents <= remaining_cents:
            selected_ids.append(row_id)
            spent_cents += cost_cents
            remaining_cents -= cost_cents
        else:
            skipped_ids.append(row_id)

    return {
        "selected_ids": selected_ids,
        "spent_cents": spent_cents,
        "remaining_cents": remaining_cents,
        "skipped_ids": skipped_ids,
    }
```