"""Series = a series-kind Tag + ONE flat, series-global ordered run of pages,
with optional cosmetic chapter DIVIDERS over it (FC-6.x reframe, #789).

Reading order is series_page.page_number alone (rewritten 1..N wholesale on any
reorder; page_number is not unique, so a reorder never transiently collides). A
chapter is NOT a container — it is a labeled divider anchored to the page that
begins it (series_chapter.anchor_page_id); a page's chapter is derived as the
nearest preceding divider. An image lives in at most one series
(series_page.image_id is UNIQUE).

All mutations are Core/set-based and run in the request transaction.
"""

from sqlalchemy import and_, func, select, update
from sqlalchemy.dialects.postgresql import insert as pg_insert
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession

from ..models import Artist, ImageRecord, Post, Tag, TagKind
from ..models.series_chapter import SeriesChapter
from ..models.series_page import SeriesPage
from .gallery_service import image_url, thumbnail_url
from .page_number_parser import parse_page_range


class SeriesError(ValueError):
    """Series validation failure. The API maps 'not a series'/'not found'
    to 404 and everything else to 400."""


class SeriesService:
    def __init__(self, session: AsyncSession):
        self.session = session

    # ---- guards / helpers -------------------------------------------------

    async def _require_series(self, series_tag_id: int) -> Tag:
        tag = await self.session.get(Tag, series_tag_id)
        if tag is None:
            raise SeriesError(f"Tag {series_tag_id} not found")
        if tag.kind != TagKind.series:
            raise SeriesError(f"Tag {series_tag_id} is not a series")
        return tag

    @staticmethod
    def _clean_ids(ids: list[int]) -> list[int]:
        if not ids:
            raise SeriesError("ids must be a non-empty list")
        if len(ids) > 500:
            raise SeriesError("selection too large (max 500)")
        seen: set[int] = set()
        out: list[int] = []
        for x in ids:
            if x not in seen:
                seen.add(x)
                out.append(int(x))
        return out

    async def _page_order(self, series_tag_id: int) -> list[int]:
        """The series' PLACED image_ids in reading order (series-global
        page_number). Pending (staged) pages are excluded — they have no order
        yet."""
        rows = (
            await self.session.execute(
                select(SeriesPage.image_id)
                .where(
                    and_(
                        SeriesPage.series_tag_id == series_tag_id,
                        SeriesPage.status == "placed",
                    )
                )
                .order_by(
                    SeriesPage.page_number.asc().nulls_last(),
                    SeriesPage.id.asc(),
                )
            )
        ).scalars().all()
        return list(rows)

    async def _page_id_for(self, series_tag_id: int, image_id: int) -> int:
        """series_page.id for an image in the series (raises if not a member)."""
        pid = await self.session.scalar(
            select(SeriesPage.id).where(
                and_(
                    SeriesPage.series_tag_id == series_tag_id,
                    SeriesPage.image_id == image_id,
                )
            )
        )
        if pid is None:
            raise SeriesError(f"image {image_id} is not in this series")
        return pid

    async def _require_divider(self, series_tag_id: int, divider_id: int):
        row = (
            await self.session.execute(
                select(SeriesChapter).where(
                    and_(
                        SeriesChapter.id == divider_id,
                        SeriesChapter.series_tag_id == series_tag_id,
                    )
                )
            )
        ).scalar_one_or_none()
        if row is None:
            raise SeriesError(
                f"chapter {divider_id} is not in series {series_tag_id}"
            )
        return row

    # ---- read -------------------------------------------------------------

    @staticmethod
    def _part_gaps(dividers: list[dict]) -> list[dict]:
        """Missing-Part hints between consecutive dividers whose stated_part
        numbers jump by more than 1 (e.g. Part 1 then Part 3). Only dividers
        that carry a stated_part participate."""
        out: list[dict] = []
        prev = None
        for d in dividers:
            cur = d["stated_part"]
            if cur is None:
                continue
            if prev is not None and cur > prev["stated_part"] + 1:
                out.append(
                    {
                        "after_divider_id": prev["id"],
                        "start": prev["stated_part"] + 1,
                        "end": cur - 1,
                    }
                )
            prev = d
        return out

    @staticmethod
    def _page_gaps(pages: list[dict]) -> list[dict]:
        """Missing-number gaps between consecutive NUMBERED placed pages — one
        entry per gap (e.g. pages 2 then 5 → one gap 3–4). The UI renders each as
        a single block whose edges accept a dropped page. Unnumbered pages (at
        the tail) don't participate."""
        out: list[dict] = []
        prev = None
        for p in pages:
            n = p["page_number"]
            if n is None:
                continue
            if prev is not None and n > prev["page_number"] + 1:
                out.append(
                    {
                        "after_image_id": prev["image_id"],
                        "before_image_id": p["image_id"],
                        "from": prev["page_number"] + 1,
                        "to": n - 1,
                    }
                )
            prev = p
        return out

    async def list_pages(self, series_tag_id: int) -> dict:
        tag = await self._require_series(series_tag_id)
        rows = (
            await self.session.execute(
                select(
                    SeriesPage.image_id,
                    SeriesPage.page_number,
                    SeriesPage.stated_page,
                    ImageRecord.sha256,
                    ImageRecord.mime,
                    ImageRecord.path,
                    ImageRecord.thumbnail_path,
                    ImageRecord.primary_post_id,
                    Post.post_title,
                )
                .select_from(SeriesPage)
                .join(ImageRecord, ImageRecord.id == SeriesPage.image_id)
                .outerjoin(Post, Post.id == ImageRecord.primary_post_id)
                .where(
                    and_(
                        SeriesPage.series_tag_id == series_tag_id,
                        SeriesPage.status == "placed",
                    )
                )
                .order_by(
                    SeriesPage.page_number.asc().nulls_last(),
                    SeriesPage.id.asc(),
                )
            )
        ).all()
        pages = [
            {
                "image_id": r.image_id,
                "page_number": r.page_number,
                "stated_page": r.stated_page,
                "thumbnail_url": thumbnail_url(
                    r.thumbnail_path, r.sha256, r.mime
                ),
                "image_url": image_url(r.path),
                "source_post": (
                    {"id": r.primary_post_id, "title": r.post_title}
                    if r.primary_post_id is not None
                    else None
                ),
            }
            for r in rows
        ]

        # Dividers, ordered by the global position of their anchor page.
        drows = (
            await self.session.execute(
                select(
                    SeriesChapter.id,
                    SeriesChapter.title,
                    SeriesChapter.stated_part,
                    SeriesPage.image_id.label("anchor_image_id"),
                    SeriesPage.page_number,
                )
                .join(SeriesPage, SeriesPage.id == SeriesChapter.anchor_page_id)
                .where(SeriesChapter.series_tag_id == series_tag_id)
                .order_by(SeriesPage.page_number.asc())
            )
        ).all()
        dividers = [
            {
                "id": r.id,
                "anchor_image_id": r.anchor_image_id,
                "page_number": r.page_number,
                "title": r.title,
                "stated_part": r.stated_part,
            }
            for r in drows
        ]

        # Pending (staged-from-post) pages, grouped by their source post so the
        # operator can drop junk and place the keepers into the run.
        prows = (
            await self.session.execute(
                select(
                    SeriesPage.image_id,
                    SeriesPage.stated_page,
                    ImageRecord.sha256,
                    ImageRecord.mime,
                    ImageRecord.path,
                    ImageRecord.thumbnail_path,
                    ImageRecord.primary_post_id,
                    Post.post_title,
                )
                .select_from(SeriesPage)
                .join(ImageRecord, ImageRecord.id == SeriesPage.image_id)
                .outerjoin(Post, Post.id == ImageRecord.primary_post_id)
                .where(
                    and_(
                        SeriesPage.series_tag_id == series_tag_id,
                        SeriesPage.status == "pending",
                    )
                )
                .order_by(
                    ImageRecord.primary_post_id.asc().nulls_last(),
                    SeriesPage.id.asc(),
                )
            )
        ).all()
        pending_groups: list[dict] = []
        by_post: dict = {}
        for r in prows:
            grp = by_post.get(r.primary_post_id)
            if grp is None:
                grp = {
                    "post": (
                        {"id": r.primary_post_id, "title": r.post_title}
                        if r.primary_post_id is not None
                        else None
                    ),
                    "start_page": None,
                    "pages": [],
                }
                by_post[r.primary_post_id] = grp
                pending_groups.append(grp)
            grp["pages"].append(
                {
                    "image_id": r.image_id,
                    "stated_page": r.stated_page,
                    "thumbnail_url": thumbnail_url(
                        r.thumbnail_path, r.sha256, r.mime
                    ),
                    "image_url": image_url(r.path),
                }
            )
            # Group start page = the post's parsed starting page (stored on the
            # staged pages); surfaced as the default for "Place from page N".
            if r.stated_page is not None:
                cur = grp["start_page"]
                grp["start_page"] = (
                    r.stated_page if cur is None else min(cur, r.stated_page)
                )

        return {
            "series": {"id": tag.id, "name": tag.name},
            "pages": pages,
            "gaps": self._page_gaps(pages),
            "dividers": dividers,
            "pending": pending_groups,
            "part_gaps": self._part_gaps(dividers),
        }

    # ---- pages ------------------------------------------------------------

    async def add_images(
        self,
        series_tag_id: int,
        image_ids: list[int],
        stated_pages: dict[int, int] | None = None,
    ) -> int:
        """Append images as pages at the END of the series' flat run. Images
        already in THIS series are left untouched; images in another series are
        moved here (image_id is UNIQUE)."""
        await self._require_series(series_tag_id)
        ids = self._clean_ids(image_ids)
        existing = dict(
            (
                await self.session.execute(
                    select(SeriesPage.image_id, SeriesPage.series_tag_id)
                    .where(SeriesPage.image_id.in_(ids))
                )
            ).all()
        )
        to_add = [i for i in ids if existing.get(i) != series_tag_id]
        if not to_add:
            return 0
        # Move: drop any prior membership for these images (UNIQUE image_id). A
        # dropped page that anchored a divider in another series cascades it away.
        await self.session.execute(
            SeriesPage.__table__.delete().where(SeriesPage.image_id.in_(to_add))
        )
        max_pn = await self._max_placed_page_number(series_tag_id)
        sp = stated_pages or {}
        await self.session.execute(
            pg_insert(SeriesPage).values(
                [
                    {
                        "series_tag_id": series_tag_id,
                        "image_id": iid,
                        "status": "placed",
                        "page_number": max_pn + offset,
                        "stated_page": sp.get(iid),
                    }
                    for offset, iid in enumerate(to_add, start=1)
                ]
            )
        )
        return len(to_add)

    async def _max_placed_page_number(self, series_tag_id: int) -> int:
        return (
            await self.session.scalar(
                select(
                    func.coalesce(func.max(SeriesPage.page_number), 0)
                ).where(
                    and_(
                        SeriesPage.series_tag_id == series_tag_id,
                        SeriesPage.status == "placed",
                    )
                )
            )
        ) or 0

    async def remove_images(
        self, series_tag_id: int, image_ids: list[int]
    ) -> int:
        await self._require_series(series_tag_id)
        ids = self._clean_ids(image_ids)
        res = await self.session.execute(
            SeriesPage.__table__.delete().where(
                and_(
                    SeriesPage.series_tag_id == series_tag_id,
                    SeriesPage.image_id.in_(ids),
                )
            )
        )
        # Dividers anchored to a removed page cascade away (chapter merges into
        # the preceding run). Page numbers are operator-set and sparse, so we do
        # NOT renumber — removing a page just leaves a gap.
        return res.rowcount or 0

    async def set_page_number(
        self, series_tag_id: int, image_id: int, page_number: int | None
    ) -> None:
        """Set one placed page's number — the operator's value: sparse, gaps
        allowed, or None to leave it unnumbered (sorts to the end). No other page
        is touched; reading order follows the numbers."""
        await self._require_series(series_tag_id)
        res = await self.session.execute(
            update(SeriesPage)
            .where(
                and_(
                    SeriesPage.series_tag_id == series_tag_id,
                    SeriesPage.image_id == image_id,
                    SeriesPage.status == "placed",
                )
            )
            .values(page_number=page_number)
        )
        if (res.rowcount or 0) == 0:
            raise SeriesError(
                f"image {image_id} is not a placed page of this series"
            )

    async def set_cover(self, series_tag_id: int, image_id: int) -> None:
        """Cover = the lowest-numbered page. Renumber the chosen image to sit
        just before the current minimum so it sorts first."""
        await self._require_series(series_tag_id)
        await self._page_id_for(series_tag_id, image_id)  # membership guard
        min_other = await self.session.scalar(
            select(func.min(SeriesPage.page_number)).where(
                and_(
                    SeriesPage.series_tag_id == series_tag_id,
                    SeriesPage.status == "placed",
                    SeriesPage.image_id != image_id,
                )
            )
        )
        new_pn = (min_other - 1) if min_other is not None else 1
        await self.session.execute(
            update(SeriesPage)
            .where(
                and_(
                    SeriesPage.series_tag_id == series_tag_id,
                    SeriesPage.image_id == image_id,
                )
            )
            .values(page_number=new_pn)
        )

    # ---- chapter dividers -------------------------------------------------

    async def create_divider(
        self,
        series_tag_id: int,
        anchor_image_id: int,
        *,
        title: str | None = None,
        stated_part: int | None = None,
    ) -> dict:
        """Place a chapter divider before the page for anchor_image_id ("a new
        chapter starts here"). One divider per page (UNIQUE anchor)."""
        await self._require_series(series_tag_id)
        page_id = await self._page_id_for(series_tag_id, anchor_image_id)
        try:
            async with self.session.begin_nested():
                res = await self.session.execute(
                    pg_insert(SeriesChapter)
                    .values(
                        series_tag_id=series_tag_id,
                        anchor_page_id=page_id,
                        title=title,
                        stated_part=stated_part,
                    )
                    .returning(SeriesChapter.id)
                )
                divider_id = res.scalar_one()
        except IntegrityError as exc:
            raise SeriesError(
                "a chapter divider already starts at that page"
            ) from exc
        return {"id": divider_id, "anchor_image_id": anchor_image_id}

    async def update_divider(
        self,
        series_tag_id: int,
        divider_id: int,
        *,
        title: str | None = None,
        stated_part: int | None = None,
        anchor_image_id: int | None = None,
        set_title: bool = False,
        set_part: bool = False,
        set_anchor: bool = False,
    ) -> None:
        """Partial divider edit. set_* flags say which fields to write (so None
        can be written explicitly, e.g. clearing a stated_part). set_anchor moves
        the divider to sit before a different page."""
        await self._require_series(series_tag_id)
        await self._require_divider(series_tag_id, divider_id)
        values: dict = {}
        if set_title:
            values["title"] = title
        if set_part:
            values["stated_part"] = stated_part
        if set_anchor:
            if anchor_image_id is None:
                raise SeriesError("anchor_image_id required to move a divider")
            values["anchor_page_id"] = await self._page_id_for(
                series_tag_id, anchor_image_id
            )
        if not values:
            return
        try:
            async with self.session.begin_nested():
                await self.session.execute(
                    update(SeriesChapter)
                    .where(SeriesChapter.id == divider_id)
                    .values(**values)
                )
        except IntegrityError as exc:
            raise SeriesError(
                "a chapter divider already starts at that page"
            ) from exc

    async def delete_divider(
        self, series_tag_id: int, divider_id: int
    ) -> None:
        """Remove a divider — the chapter merges into the run above it. Pages
        are untouched (dividers own nothing)."""
        await self._require_series(series_tag_id)
        await self._require_divider(series_tag_id, divider_id)
        await self.session.execute(
            SeriesChapter.__table__.delete().where(
                SeriesChapter.id == divider_id
            )
        )

    # ---- post → series (FC-6.2) ------------------------------------------

    async def _post_images_ordered(self, post_id: int) -> list[int]:
        """A post's images in capture order (ImageRecord.primary_post_id), the
        same ordering the posts feed renders thumbnails in."""
        rows = (
            await self.session.execute(
                select(ImageRecord.id)
                .where(ImageRecord.primary_post_id == post_id)
                .order_by(ImageRecord.id.asc())
            )
        ).scalars().all()
        return list(rows)

    @staticmethod
    def _stated_map(image_ids: list[int], start: int | None) -> dict | None:
        """Per-image stated_page = start, start+1, ... when the post stated a
        starting page; None when it didn't (fall back to capture order)."""
        if start is None:
            return None
        return {iid: start + i for i, iid in enumerate(image_ids)}

    async def promote_post_to_series(self, post_id: int) -> dict:
        """Case 1: a self-contained multi-image post becomes its own series.
        Creates a series tag named after the post and the post's images as its
        flat pages (stated pages parsed from the post when present)."""
        from .tag_service import TagService

        post = await self.session.get(Post, post_id)
        if post is None:
            raise SeriesError(f"post {post_id} not found")
        image_ids = await self._post_images_ordered(post_id)
        if not image_ids:
            raise SeriesError("post has no images to make a series from")
        name = (post.post_title or f"Series from post {post_id}").strip()[:200]
        tag = await TagService(self.session).find_or_create(name, TagKind.series)
        rng = parse_page_range(f"{post.post_title or ''} {post.description or ''}")
        start = rng[0] if rng else None
        added = await self.add_images(
            tag.id, image_ids, stated_pages=self._stated_map(image_ids, start),
        )
        return {
            "series_tag_id": tag.id, "name": tag.name, "added": added,
        }

    async def add_post(self, series_tag_id: int, post_id: int) -> dict:
        """Case 2: STAGE a post's images as PENDING pages of an existing series
        (#789 P2). They don't enter the ordered run yet — the operator drops junk
        (text-free alts, bumpers) and places the keepers via place_pending, so
        the series-global numbering stays clean. Stated pages are parsed from the
        post when present."""
        await self._require_series(series_tag_id)
        post = await self.session.get(Post, post_id)
        if post is None:
            raise SeriesError(f"post {post_id} not found")
        image_ids = await self._post_images_ordered(post_id)
        if not image_ids:
            raise SeriesError("post has no images to add")
        # Skip images already in THIS series (placed or pending); move any in
        # another series here as pending (image_id is UNIQUE).
        existing = dict(
            (
                await self.session.execute(
                    select(SeriesPage.image_id, SeriesPage.series_tag_id)
                    .where(SeriesPage.image_id.in_(image_ids))
                )
            ).all()
        )
        to_stage = [i for i in image_ids if existing.get(i) != series_tag_id]
        if not to_stage:
            return {"series_tag_id": series_tag_id, "staged": 0}
        await self.session.execute(
            SeriesPage.__table__.delete().where(
                SeriesPage.image_id.in_(to_stage)
            )
        )
        # Store the post's parsed START page on every staged page (constant), so
        # the group's start survives junk removal and seeds "Place from page N".
        rng = parse_page_range(f"{post.post_title or ''} {post.description or ''}")
        start = rng[0] if rng else None
        await self.session.execute(
            pg_insert(SeriesPage).values(
                [
                    {
                        "series_tag_id": series_tag_id,
                        "image_id": iid,
                        "status": "pending",
                        "page_number": None,
                        "stated_page": start,
                    }
                    for iid in to_stage
                ]
            )
        )
        return {"series_tag_id": series_tag_id, "staged": len(to_stage)}

    async def place_pending(
        self,
        series_tag_id: int,
        image_ids: list[int],
        start_page: int | None = None,
    ) -> int:
        """Place staged (pending) pages into the run, numbering them sequentially
        from `start_page` in the given order (start_page, start_page+1, …). With
        start_page None they're placed unnumbered (sorting to the tail). Drop the
        junk BEFORE placing so the numbers line up; only pending pages are
        touched, and other pages keep their numbers."""
        await self._require_series(series_tag_id)
        ids = self._clean_ids(image_ids)
        rows = dict(
            (
                await self.session.execute(
                    select(SeriesPage.image_id, SeriesPage.status).where(
                        and_(
                            SeriesPage.series_tag_id == series_tag_id,
                            SeriesPage.image_id.in_(ids),
                        )
                    )
                )
            ).all()
        )
        # Preserve the caller's order, restricted to actually-pending pages.
        staged = [i for i in ids if rows.get(i) == "pending"]
        if not staged:
            return 0
        for offset, iid in enumerate(staged):
            await self.session.execute(
                update(SeriesPage)
                .where(
                    and_(
                        SeriesPage.series_tag_id == series_tag_id,
                        SeriesPage.image_id == iid,
                    )
                )
                .values(
                    status="placed",
                    page_number=(
                        start_page + offset if start_page is not None else None
                    ),
                )
            )
        return len(staged)

    # ---- browse list (FC-6.2) --------------------------------------------

    async def list_series(
        self, *, sort: str = "recent", artist_id: int | None = None
    ) -> list[dict]:
        """Series cards for the browse view: cover thumb, name, artist, divider
        + page counts, a missing-Part flag, and last-updated. sort ∈
        recent|name|size."""
        # Page counts + most-recent page activity per series.
        page_rows = (
            await self.session.execute(
                select(
                    SeriesPage.series_tag_id,
                    func.count().label("pages"),
                    func.max(SeriesPage.updated_at).label("updated_at"),
                )
                .where(SeriesPage.status == "placed")
                .group_by(SeriesPage.series_tag_id)
            )
        ).all()
        page_count = {r.series_tag_id: r.pages for r in page_rows}
        updated_by_series = {r.series_tag_id: r.updated_at for r in page_rows}

        # Dividers per series (for count + missing-Part gap), ordered by the
        # global position of their anchor page.
        drows = (
            await self.session.execute(
                select(
                    SeriesChapter.series_tag_id,
                    SeriesChapter.id,
                    SeriesChapter.stated_part,
                    SeriesPage.page_number,
                )
                .join(SeriesPage, SeriesPage.id == SeriesChapter.anchor_page_id)
                .order_by(
                    SeriesChapter.series_tag_id, SeriesPage.page_number.asc()
                )
            )
        ).all()
        dividers_by_series: dict[int, list] = {}
        for r in drows:
            dividers_by_series.setdefault(r.series_tag_id, []).append(r)

        # Cover = the first page (min page_number) of each series.
        cover_q = (
            select(
                SeriesPage.series_tag_id,
                ImageRecord.sha256,
                ImageRecord.mime,
                ImageRecord.thumbnail_path,
                ImageRecord.artist_id,
                func.row_number()
                .over(
                    partition_by=SeriesPage.series_tag_id,
                    order_by=(
                        SeriesPage.page_number.asc(), SeriesPage.id.asc()
                    ),
                )
                .label("rn"),
            )
            .join(ImageRecord, ImageRecord.id == SeriesPage.image_id)
            .where(SeriesPage.status == "placed")
            .subquery()
        )
        cover_rows = (
            await self.session.execute(
                select(
                    cover_q.c.series_tag_id,
                    cover_q.c.sha256,
                    cover_q.c.mime,
                    cover_q.c.thumbnail_path,
                    cover_q.c.artist_id,
                ).where(cover_q.c.rn == 1)
            )
        ).all()
        cover_by_series = {r.series_tag_id: r for r in cover_rows}

        # Artist names for the covers.
        artist_ids = {
            r.artist_id for r in cover_rows if r.artist_id is not None
        }
        artist_by_id: dict[int, object] = {}
        if artist_ids:
            arows = (
                await self.session.execute(
                    select(Artist.id, Artist.name, Artist.slug)
                    .where(Artist.id.in_(artist_ids))
                )
            ).all()
            artist_by_id = {a.id: a for a in arows}

        tags = (
            await self.session.execute(
                select(Tag.id, Tag.name).where(Tag.kind == TagKind.series)
            )
        ).all()

        out: list[dict] = []
        for t in tags:
            cover = cover_by_series.get(t.id)
            if artist_id is not None and (
                cover is None or cover.artist_id != artist_id
            ):
                continue
            divs = dividers_by_series.get(t.id, [])
            gap = bool(
                self._part_gaps(
                    [{"id": d.id, "stated_part": d.stated_part} for d in divs]
                )
            )
            artist = artist_by_id.get(cover.artist_id) if cover else None
            updated = updated_by_series.get(t.id)
            out.append(
                {
                    "id": t.id,
                    "name": t.name,
                    "cover_thumbnail_url": (
                        thumbnail_url(
                            cover.thumbnail_path, cover.sha256, cover.mime
                        )
                        if cover
                        else None
                    ),
                    "artist_name": artist.name if artist else None,
                    "artist_slug": artist.slug if artist else None,
                    "chapter_count": len(divs),
                    "page_count": page_count.get(t.id, 0),
                    "has_gap": gap,
                    "updated_at": updated.isoformat() if updated else None,
                }
            )

        if sort == "name":
            out.sort(key=lambda s: s["name"].lower())
        elif sort == "size":
            out.sort(key=lambda s: s["page_count"], reverse=True)
        else:  # recent
            out.sort(key=lambda s: s["updated_at"] or "", reverse=True)
        return out
