"""Bulk, idempotent writes to the ``image_tag`` association table.

Three writers attach tags to images in bulk: the WIP-title backfill
(`wip_title.apply_wip_image_tags`), the concept-head auto-apply sweep and the
system-tag auto-apply sweep (both in `ml/heads.py`). The two sweeps used to
issue ONE INSERT PER ROW from inside their per-image loop — fine in steady
state, but a first pass over a back-catalogue is tens of thousands of
individual round-trips (#3072). All three share this one chunked multi-row
insert now.

Sync only: every caller runs on a sync ``Session`` (the Celery task path). No
async service writes image_tag in bulk, so there is no async sibling to keep in
step — unlike `db_helpers.get_or_create`, which does have one.
"""

from __future__ import annotations

from sqlalchemy.dialects.postgresql import insert as pg_insert
from sqlalchemy.orm import Session

from ..models.tag import image_tag

# 5000 rows x 3 bound params = 15000, comfortably inside Postgres' 65535-param
# ceiling for a single statement. Raising this past ~21000 rows would exceed it.
INSERT_CHUNK = 5000


def insert_image_tags(
    session: Session, rows: list[dict], *, chunk: int = INSERT_CHUNK
) -> None:
    """Attach ``rows`` to their images, skipping any tag already on one.

    Each row is ``{"image_record_id": int, "tag_id": int, "source": str}``.
    Does NOT commit — the caller owns the transaction.

    ON CONFLICT DO NOTHING against the (image_record_id, tag_id) primary key,
    so an existing tag keeps its ORIGINAL ``source``: re-running a sweep can
    never re-stamp a tag the operator applied by hand as machine-applied.

    Returns nothing on purpose. psycopg reports ``rowcount`` -1 for a multi-row
    ON CONFLICT DO NOTHING insert (it runs via an executemany path), so a count
    taken from the statement would be a lie rather than an approximation.
    Callers that need an accurate count derive it themselves — see
    `wip_title.apply_wip_image_tags`' pre-SELECT, and the sweeps' `skip` sets.
    """
    for start in range(0, len(rows), chunk):
        session.execute(
            pg_insert(image_tag)
            .values(rows[start:start + chunk])
            .on_conflict_do_nothing(index_elements=["image_record_id", "tag_id"])
        )
