"""Python beside the imdb recipe: the GraphQL door, and nothing else.

**What is here and what stayed in the JSON is the whole point of the split.** IMDb keeps
its data behind one endpoint that answers a query — protocol knowledge, which no gesture
composes and which reads worse as a string inside a JSON field than as code
(`docs/specs/spec-parser-model.md` §10). Everything *after* the answer — which paths hold
the images, which track to prefer, how the cursor threads the next page — is expressible,
visible in the constructor, and stays there.

**Three subjects, one door.** A film (`/title/tt…`), a person (`/name/nm…`) and a gallery
(`/gallery/rg…`) are the same endpoint asked about different entities, and the only thing
that differs is which query goes in the body. Eight queries and one hook; the recipe's
nodes are untouched by this file.
"""

from nd_crawler import SiteHandler

DOOR = "https://caching.graphql.imdb.com/"

#: The key to this endpoint is `referer`, not `origin` — measured, and the opposite of what
#: an earlier note in the journal said (`docs/LOG.md` 242). Without it: 403.
POLITE = {"referer": "https://www.imdb.com/"}

# ---- what opens a subject ----------------------------------------------------

TITLE = "query Title($id: ID!) { title(id: $id) { titleText { text } } }"
PERSON = "query Person($id: ID!) { name(id: $id) { nameText { text } } }"

#: A gallery's own heading, and the only thing on `ImageGallery` worth having: the type
#: carries no title, no name and no count of its own — measured by asking for each and
#: being refused by name. `galleryText` is a plain String, not a `{ text }` wrapper like
#: everywhere else on this schema.
GALLERY = "query Gallery($id: ID!) { imageGallery(id: $id) { galleryText } }"

# ---- images: these connections page honestly ---------------------------------

# The cursor threads and `hasNextPage` tells the truth — worth saying out loud beside the
# video queries below, which do neither.
#
# **`caption` is asked for because it is what a person reads.** Measured 2026-08-20 over
# 493 images across a film, two people and a gallery: *every one of them* carried a
# caption, median 45 characters, longest 394. It is both the name and the description —
# the name because `rm2882842625.jpg` tells nobody anything, the description because
# roughly one caption in twenty runs past the 120 characters a name is cut to, and the
# description is where the rest survives.
IMAGES = (
    "query Images($id: ID!, $after: ID) { title(id: $id) { "
    "images(first: 50, after: $after) { pageInfo { hasNextPage endCursor } "
    "edges { node { id url width height caption { plainText } } } } } }"
)

PERSON_IMAGES = (
    "query PersonImages($id: ID!, $after: ID) { name(id: $id) { "
    "images(first: 50, after: $after) { pageInfo { hasNextPage endCursor } "
    "edges { node { id url width height caption { plainText } } } } } }"
)

GALLERY_IMAGES = (
    "query GalleryImages($id: ID!, $after: ID) { imageGallery(id: $id) { "
    "images(first: 50, after: $after) { pageInfo { hasNextPage endCursor } "
    "edges { node { id url width height caption { plainText } } } } } }"
)

# ---- videos: these do not page at all ----------------------------------------

# **`first: 100` and no `after`, because this connection cannot be paged.** Measured
# 2026-08-20: it answers `hasNextPage: false` whatever it is asked, hands back an
# `endCursor` of a single space, and ignores `after` — fed its own cursor it returns page
# one again. So the page size *is* the whole result, and 100 is the ceiling the server
# enforces: asking for 250 still returns 100. It said `first: 25` until today, which is
# why a film with more clips than that quietly yielded 25 of them and looked fine.
#
# The ceiling is real and the recipe cannot get past it: a person with 255 videos —
# measured on Tom Hanks — is reachable only for the first hundred.
# A clip's `description` is the other real supplier of our description field: filled on
# 216 of the 218 clips measured, median 140 characters and up to 729 — a synopsis, not a
# label, which is exactly the half that has nowhere else to go.
VIDEOS = (
    "query Videos($id: ID!) { title(id: $id) { "
    "videoStrip(first: 100) { edges { node { id name { value } description { value } "
    "runtime { value } contentType { displayName { value } } createdDate "
    "playbackURLs { videoDefinition videoMimeType url } } } } } }"
)

#: A person's videos live under `videos`, not `videoStrip` — the schema refuses the title
#: spelling by name — and share both of its lies, so: the same one shot.
PERSON_VIDEOS = (
    "query PersonVideos($id: ID!) { name(id: $id) { "
    "videos(first: 100) { edges { node { id name { value } description { value } "
    "runtime { value } contentType { displayName { value } } createdDate "
    "playbackURLs { videoDefinition videoMimeType url } } } } } }"
)

# ---- who answers what --------------------------------------------------------

#: What each kind of step asks for, and **which id it asks about** — the two written as one
#: entry rather than looked up separately. The kinds are the recipe's own, written into the
#: payload by the node that queued the step; the pairing lives here because the query is
#: the half no gesture composes.
#:
#: **Pairing them structurally is what stops them diverging.** A person id sent to a title
#: query is answered with `null` and no error — a run that finds nothing and reports no
#: fault — and holding the two in separate tables makes that a matter of keeping them in
#: step by hand.
ASKING = {
    "images": ("film", IMAGES),
    "videos": ("film", VIDEOS),
    "person-images": ("who", PERSON_IMAGES),
    "person-videos": ("who", PERSON_VIDEOS),
    "gallery-images": ("gallery", GALLERY_IMAGES),
}

#: What opens a subject, before the step has asked for anything in particular. Tried in
#: order, and only one is ever set: an opening node writes its own id and no other.
OPENING = (("film", TITLE), ("who", PERSON), ("gallery", GALLERY))


class Imdb(SiteHandler):
    """One hook. The rest of the recipe is declarative and shows in the constructor."""

    def ask(self, step, engine):
        """Every step of this site is the same endpoint asked a different question.

        **The id comes from the payload, and on the first step from the address.** Each
        opening node catches its own out of the path — `/title/(?P<film>tt\\d+)`,
        `/name/(?P<who>nm\\d+)`, `/gallery/(?P<gallery>rg\\d+)` — and writes it into the
        payload of everything it queues, so this hook never has to know the shape of an
        IMDb URL. It is also what makes a link to one photo inside a gallery fetch the
        whole gallery: the pattern catches the `rg` id and pays no attention to the
        `/mediaviewer/rm…` that follows it.

        **A step that says what it wants names its own subject**; one that says nothing is
        opening one, and then whichever id was caught decides. Either way the query and the
        id it is about come out of the same entry, so they cannot be paired wrongly.
        """
        kind = step.payload.get("kind", "")
        wanted = [ASKING[kind]] if kind in ASKING else list(OPENING)
        for subject, query in wanted:
            found = step.payload.get(subject) or engine.caught(subject)
            if found:
                return engine.request(
                    DOOR,
                    method="POST",
                    headers=POLITE,
                    body={
                        "query": query,
                        "variables": {"id": str(found), "after": step.cursor},
                    },
                )
        # Not ours: an address on imdb.com that is none of the three, or a step asking for
        # something about a subject whose id nobody wrote down. Let the nodes decide, which
        # for this recipe means nobody claims it — and that is the honest answer.
        return None
