"""PlatformInfo dataclass + shared defaults + small helpers.

Per-platform modules import from here, register their PlatformInfo via
INFO, optionally attaching `derive_post_url` and/or `augment_cookies`
callables for behavior that diverges from gallery-dl's mainline shape
(Patreon).

Adding a new platform: drop a new module under `services/platforms/`,
declare an INFO, and add it to the import list in
`services/platforms/__init__.py`. Sidecar parsing, cookie
materialization, and the /api/platforms response pick it up
automatically.
"""

from collections.abc import Callable
from dataclasses import dataclass
from typing import Literal

# Sidecar parsing defaults. Per-platform PlatformInfo entries can
# override these by setting `external_post_id_keys=` /
# `description_keys=`. Most don't need to — the defaults already cover
# every platform FC supports.
#
# external_post_id chain: `post_id` MUST come before `id` because
# SubscribeStar gallery-dl puts the per-attachment id in `id` and the
# actual post id in `post_id`; picking `id` first fragments
# multi-image SubscribeStar posts into N Post rows. Patreon/Pixiv have
# no `post_id` so `id` still wins for them; HF uses `index`, Discord
# uses `message_id` — all reached via the remaining chain entries.
# (Banked 2026-05-27 during the sidecar audit.)
DEFAULT_EXTERNAL_POST_ID_KEYS: tuple[str, ...] = (
    "post_id", "id", "index", "message_id",
)

# Description body chain: Discord's gallery-dl extractor uses `message`
# (no `content`); appended to the chain so Discord posts surface body
# text.
DEFAULT_DESCRIPTION_KEYS: tuple[str, ...] = (
    "content", "description", "caption", "message",
)


@dataclass(frozen=True)
class PlatformInfo:
    # --- Identity / metadata ---
    key: str
    name: str
    description: str
    auth_type: Literal["cookies", "token"]
    requires_auth: bool
    url_pattern: str
    url_examples: list[str]
    default_config: dict
    notes: str | None = None

    # --- Sidecar parsing overrides ---
    # Each is None to mean "use the module default above"; a platform
    # only sets one of these when its sidecar shape genuinely differs.
    external_post_id_keys: tuple[str, ...] | None = None
    description_keys: tuple[str, ...] | None = None

    # --- Behavioral hooks ---
    # Synthesize a post permalink from sidecar data. Required when
    # gallery-dl's `url` field is the file/CDN URL rather than the post
    # permalink (subscribestar/pixiv/hf/discord). None = trust the bare
    # `url` field (patreon).
    derive_post_url: Callable[[dict], str | None] | None = None

    # Post-process the materialized cookies.txt for gallery-dl. Used by
    # platforms whose server gates or extractor quirks need synthetic
    # cookies the extension can't capture (subscribestar age cookie, HF
    # host-only PHPSESSID duplicate). None = no-op.
    augment_cookies: Callable[[str], str] | None = None


def str_id_value(v) -> str | None:
    """Coerce a JSON scalar id into a non-empty string, rejecting bool
    (Python's bool is an int subclass so `isinstance(True, int)` is
    True; without this guard a sidecar with `"id": true` would produce
    external_post_id="True")."""
    if isinstance(v, bool):
        return None
    if isinstance(v, (str, int)) and str(v).strip():
        return str(v).strip()
    return None


def str_field(v) -> str | None:
    """Same idea as str_id_value but for plain string fields (no int
    coercion)."""
    if isinstance(v, str) and v.strip():
        return v.strip()
    return None


# Shared gallery-dl invocation defaults. Embedded in each platform's
# default_config (with platform-specific overrides) so per-platform
# choices stay explicit.
# Note: the gallery-dl `skip` value (tick "exit:20" vs backfill True) is
# NOT here — it's derived from Source.backfill_runs_remaining at download
# time. See plan #544 / gallery_dl.TICK_SKIP_VALUE,BACKFILL_SKIP_VALUE.
GD_DEFAULTS = {
    "sleep": 3.0,
    "sleep_request": 1.5,
    "save_metadata": True,
    "timeout": 3600,
}
