import logging
from collections.abc import Sequence
from typing import Annotated, Any, cast

from fastapi import APIRouter, Depends, HTTPException, Path, Query
from qualibrate_config.models import QualibrateConfig, StorageType

from qualibrate.app.api.core.domain.bases.branch import (
    BranchBase,
    BranchLoadType,
)
from qualibrate.app.api.core.domain.bases.node import NodeLoadType
from qualibrate.app.api.core.domain.bases.snapshot import (
    SnapshotLoadTypeFlag,
)
from qualibrate.app.api.core.domain.local_storage.branch import (
    BranchLocalStorage,
)
from qualibrate.app.api.core.domain.timeline_db.branch import BranchTimelineDb
from qualibrate.app.api.core.models.branch import Branch as BranchModel
from qualibrate.app.api.core.models.node import Node as NodeModel
from qualibrate.app.api.core.models.paged import PagedCollection
from qualibrate.app.api.core.models.snapshot import (
    QubitOutcome,
    SimplifiedSnapshotWithMetadata,
    SimplifiedSnapshotWithMetadataAndOutcomes,
    SnapshotHistoryItem,
    SnapshotSearchResult,
)
from qualibrate.app.api.core.models.snapshot import Snapshot as SnapshotModel
from qualibrate.app.api.core.types import (
    IdType,
    PageFilter,
    SearchFilter,
    SearchWithIdFilter,
    SortField,
)
from qualibrate.app.api.core.utils.slice import get_page_slice
from qualibrate.app.api.dependencies.search import get_search_path
from qualibrate.app.api.routes.utils.dependencies import (
    get_page_filter,
    get_search_filter,
    get_snapshot_load_type_flag,
)
from qualibrate.app.api.routes.utils.snapshot_history import (
    build_snapshot_tree,
    paginate_nested_items,
)
from qualibrate.app.api.routes.utils.sorting import get_sort_key
from qualibrate.app.config import get_settings

logger = logging.getLogger(__name__)

# Maximum page size for fetching all items when sorting/grouping is required.
# This is needed because sorting and grouping require all items in memory.
MAX_PAGE_SIZE_FOR_GROUPING = 100000

branch_router = APIRouter(prefix="/branch/{name}", tags=["branch"])


def _filter_snapshots_by_tags(
    snapshots: list[SimplifiedSnapshotWithMetadata],
    required_tags: list[str],
) -> list[SimplifiedSnapshotWithMetadata]:
    """Filter snapshots to only those containing all required tags.

    Args:
        snapshots: List of snapshots to filter.
        required_tags: List of tag names that must all be present.

    Returns:
        Filtered list of snapshots that have ALL required tags.
    """
    if not required_tags:
        return snapshots

    required_set = set(required_tags)
    result = []
    for snapshot in snapshots:
        snapshot_tags: list[Any] = []
        if snapshot.metadata:
            # Get tags from metadata - handle both dict and object access
            metadata_dict = snapshot.metadata.model_dump()
            snapshot_tags = metadata_dict.get("tags", []) or []

        if required_set.issubset(set(snapshot_tags)):
            result.append(snapshot)

    return result


def _get_branch_instance(
    name: Annotated[
        str,
        Path(
            description=("Branch name used to resolve which branch to operate on."),
        ),
    ],
    settings: Annotated[QualibrateConfig, Depends(get_settings)],
) -> BranchBase:
    branch_types = {
        StorageType.local_storage: BranchLocalStorage,
        StorageType.timeline_db: BranchTimelineDb,
    }
    return branch_types[settings.storage.type](name=name, settings=settings)


def _compute_aggregated_outcomes_for_workflow(
    branch: BranchBase,
    metadata: dict[str, Any],
) -> dict[str, QubitOutcome] | None:
    """Compute aggregated outcomes for a workflow snapshot.

    For workflow snapshots that have children, this function recursively loads
    all child snapshots and aggregates their outcomes with failure tracking.

    Args:
        branch: The branch instance to load child snapshots.
        metadata: The workflow snapshot's metadata containing 'children' list.

    Returns:
        Aggregated outcomes dict like {"q1": {"status": "success"},
        "q2": {"status": "failure", "failed_on": "cal_node"}} or None
        if no outcomes found.
    """
    children_ids = metadata.get("children")
    if not children_ids or not isinstance(children_ids, list):
        return None

    aggregated_outcomes: dict[str, QubitOutcome] = {}

    def _collect_outcomes_recursive(
        snapshot_ids: list[IdType],
    ) -> None:
        """Recursively collect outcomes from snapshots and their children."""
        for snapshot_id in snapshot_ids:
            try:
                child_snapshot = branch.get_snapshot(snapshot_id)
                child_snapshot.load_from_flag(SnapshotLoadTypeFlag.Metadata | SnapshotLoadTypeFlag.DataWithoutRefs)
                child_content = child_snapshot.content
                child_metadata = child_content.get("metadata", {})
                child_data = child_content.get("data", {})
                node_name = child_metadata.get("name", f"node_{snapshot_id}")
                node_status = child_metadata.get("status", "")

                logger.info(
                    f"Processing child {snapshot_id}: name={node_name}, "
                    f"status={node_status}, has_children={bool(child_metadata.get('children'))}"
                )

                # Check if this child has its own children (nested workflow)
                child_children = child_metadata.get("children")
                if child_children and isinstance(child_children, list):
                    # Recursively collect from nested workflow
                    _collect_outcomes_recursive(child_children)
                else:
                    # This is a leaf node - collect its outcomes
                    node_failed = node_status in ("error", "failed")
                    raw_outcomes = child_data.get("outcomes") if child_data else None

                    logger.info(f"  Leaf node {snapshot_id}: node_failed={node_failed}, raw_outcomes={raw_outcomes}")

                    if raw_outcomes and isinstance(raw_outcomes, dict):
                        for qubit, outcome_value in raw_outcomes.items():
                            outcome_str = str(outcome_value).lower()

                            # If node has error/failed status, mark all qubits as failed
                            if node_failed:
                                if qubit not in aggregated_outcomes or aggregated_outcomes[qubit].status != "failure":
                                    aggregated_outcomes[qubit] = QubitOutcome(status="failure", failed_on=node_name)
                                    logger.info(
                                        f"  Marking {qubit} as FAILED (node status={node_status}) on {node_name}"
                                    )
                            elif outcome_str in ("successful", "success"):
                                if qubit not in aggregated_outcomes:
                                    aggregated_outcomes[qubit] = QubitOutcome(status="success")
                            elif outcome_str in ("failed", "failure", "error") and (
                                qubit not in aggregated_outcomes or aggregated_outcomes[qubit].status != "failure"
                            ):
                                # Keep the first failure (the node that failed first)
                                aggregated_outcomes[qubit] = QubitOutcome(status="failure", failed_on=node_name)
                                logger.info(f"  Marking {qubit} as FAILED (outcome={outcome_str}) on {node_name}")
                    elif node_failed:
                        # Node has error status but no outcomes recorded
                        # This happens when a node throws an exception before recording outcomes
                        # Mark ALL qubits we've seen so far as potentially failed on this node
                        # if they were previously successful
                        logger.info(
                            f"  Node {node_name} has error status but no outcomes - "
                            f"marking existing successful qubits as failed"
                        )
                        for qubit in list(aggregated_outcomes.keys()):
                            if aggregated_outcomes[qubit].status == "success":
                                aggregated_outcomes[qubit] = QubitOutcome(status="failure", failed_on=node_name)
                                logger.info(f"  Overriding {qubit} to FAILED on {node_name}")
            except Exception as e:
                logger.warning(f"Failed to load child snapshot {snapshot_id}: {e}")

    _collect_outcomes_recursive(children_ids)
    return aggregated_outcomes if aggregated_outcomes else None


@branch_router.get(
    "/",
    summary="Get branch by name",
)
def get(
    *,
    load_type: Annotated[
        BranchLoadType,
        Query(description="Level of detail to load for the branch."),
    ] = BranchLoadType.Full,
    branch: Annotated[BranchBase, Depends(_get_branch_instance)],
) -> BranchModel:
    """
    Retrieve a branch by name and load it with the requested level of detail.
    Returns serialized branch object (`BranchModel`) according to the selected
    `load_type`.

    ### Examples

    **Request:** `/branch/init_project?load_type=1`

    **Response:**
    ```json
    {
      "created_at": "2025-09-02T16:33:35+03:00",
      "id": 1,
      "name": "main",
      "snapshot_id": -1
    }
    ```
    """
    branch.load(load_type)
    return branch.dump()


@branch_router.get("/snapshot", summary="Get snapshot by ID")
def get_snapshot(
    *,
    snapshot_id: Annotated[
        IdType,
        Query(description="Identifier of the snapshot within the branch timeline."),
    ],
    load_type_flag: Annotated[SnapshotLoadTypeFlag, Depends(get_snapshot_load_type_flag)],
    branch: Annotated[BranchBase, Depends(_get_branch_instance)],
) -> SnapshotModel:
    """
    Loads and returns a single snapshot from the branch timeline using its
    identifier and the requested load-type flags.

    Returns  serialized snapshot (SnapshotModel) according to `load_type_flag`

    ### Example

    **Request:**
    `/branch/init_project/snapshot?snapshot_id=366\
    &load_type_flag=Metadata&load_type_flag=DataWithoutRefs`

    **Response:**
    ```json
    {
      "id": 366,
      "created_at": "2025-08-22T10:54:07+03:00",
      "metadata": {
        "description": "Test calibration that wait a few seconds, \
then plots random data.",
        "status": null,
        "run_start": "2025-08-22T10:54:01.247+03:00",
        "run_end": "2025-08-22T10:54:07.296+03:00",
        "run_duration": 6.049
        "name": "test_cal",
        "data_path": "2025-08-22/#366_test_cal_105407",
      },
      "data": {
        "parameters": {
          "model": {
            "qubits": ["q1","q2"],
            "resonator": "q1.resonator",
            "sampling_points": 100
            },
          "schema": { "title": "Parameters", "type": "object" }
        },
        "outcomes": { "q1": "successful", "q2": "successful" },
        "quam": "./quam_state"
      },
      "parents": [365]
    }
    ```

    For workflow snapshots with children, `data.outcomes` will contain
    outcomes aggregated from all child nodes with failure tracking:
    ```json
    {
      "data": {
        "outcomes": {
          "q1": {"status": "success", "failed_on": null},
          "q2": {"status": "failure", "failed_on": "resonator_spectroscopy"}
        }
      }
    }
    ```
    """
    snapshot = branch.get_snapshot(snapshot_id)
    snapshot.load_from_flag(load_type_flag)
    result = snapshot.dump()

    # For workflow snapshots (those with children), compute aggregated outcomes
    # and put them in data.outcomes with failed_on tracking
    metadata = snapshot.content.get("metadata", {})
    children = metadata.get("children")
    if children and isinstance(children, list) and len(children) > 0:
        aggregated_outcomes = _compute_aggregated_outcomes_for_workflow(branch, metadata)
        if aggregated_outcomes:
            # Put aggregated outcomes in data.outcomes
            if result.data is None:
                from qualibrate.app.api.core.models.snapshot import SnapshotData

                result.data = SnapshotData()
            result.data.outcomes = {qubit: outcome.model_dump() for qubit, outcome in aggregated_outcomes.items()}
            logger.debug(f"Computed outcomes with failed_on for workflow {snapshot_id}: {result.data.outcomes}")

    return result


@branch_router.get("/snapshot/latest", summary="Get latest snapshot")
def get_latest_snapshot(
    *,
    load_type_flag: Annotated[SnapshotLoadTypeFlag, Depends(get_snapshot_load_type_flag)],
    branch: Annotated[BranchBase, Depends(_get_branch_instance)],
) -> SnapshotModel:
    """Get latest snapshot.

    Returns the most recent snapshot for the branch according to the underlying
    storage ordering.

    ### Example

    **Request:**
    `/branch/main/snapshot/latest?load_type_flag=Metadata`

    **Response:**
    ```json
    {
        "created_at": "2025-08-22T12:16:42+03:00",
        "id": 367,
        "parents": [366],
        "metadata": {
            "name": "wf_node1",
            "description": null,
            "status": null,
            "run_start": "2025-08-22T12:16:38.123000+03:00",
            "run_end": "2025-08-22T12:16:42.134000+03:00",
            "data_path": "2025-08-22/#367_wf_node1_121642",
            "run_duration": 4.011
        },
        "data": null
    }
    ```
    """
    snapshot = branch.get_snapshot()
    snapshot.load_from_flag(load_type_flag)
    result = snapshot.dump()

    # For workflow snapshots (those with children), compute aggregated outcomes
    # and put them in data.outcomes with failed_on tracking
    metadata = snapshot.content.get("metadata", {})
    children = metadata.get("children")
    if children and isinstance(children, list) and len(children) > 0:
        aggregated_outcomes = _compute_aggregated_outcomes_for_workflow(branch, metadata)
        if aggregated_outcomes:
            # Put aggregated outcomes in data.outcomes
            if result.data is None:
                from qualibrate.app.api.core.models.snapshot import SnapshotData

                result.data = SnapshotData()
            result.data.outcomes = {qubit: outcome.model_dump() for qubit, outcome in aggregated_outcomes.items()}

    return result


@branch_router.get("/snapshot/filter", summary="Get first matching snapshot")
def get_snapshot_filtered(
    *,
    search_filters: Annotated[SearchFilter, Depends(get_search_filter)],
    descending: Annotated[
        bool,
        Query(description=("When true, prefer the newest match; otherwise the oldest.")),
    ] = True,
    branch: Annotated[BranchBase, Depends(_get_branch_instance)],
) -> SnapshotModel | None:
    """
    Retrieve the newest (or oldest) snapshot that satisfies the provided
    criteria. Order is controlled by the `descending` flag. Use
    `/snapshots/filter` to get a paginated list of matches.

    Returns the newest (or oldest) matching snapshot, or `null` if nothing
    matches.

    ### Examples
    #### 1)
    **Request:**
    `/branch/main/snapshot/filter?descending=true`

    Find latest snapshot.

    **Response:**
    ```json
    {
      "created_at": "2025-08-22T12:16:42+03:00",
      "id": 367,
      "parents": [366],
      "metadata": ...,
      "data": null
    }
    ```

    #### 2)
    **Request:**
    `/branch/main/snapshot/filter?descending=true&name_part=test_cal`

    Find latest snapshot with name contains `test_cal`.

    **Response:**
    ```json
    {
      "created_at": "2025-08-22T10:54:07+03:00",
      "id": 366,
      "parents": [365],
      "metadata": ...,
      "data": null
    }
    ```
    #### 3)
    **Request:**
    `/branch/main/snapshot/filter?descending=true&\
name_part=test_cal&max_node_id=365`

    Find latest snapshot with name contains `test_cal` and id less or
    equal to 365.

    **Response:**
    ```json
    {
      "created_at": "2025-08-21T14:12:28+03:00",
      "id": 365,
      "parents": [364],
      "metadata": ...,
      "data": null
    }
    ```
    #### 4)
    **Request:**
    `/branch/main/snapshot/filter?descending=true&\
name_part=test_cal&max_node_id=365&max_date=2025-08-20`

    Find latest snapshot with :
    - name contains `test_cal`,
    - id less or equal to 365,
    - created earlier than 2025-08-20

    **Response:**
    ```json
    {
      "created_at": "2025-08-18T15:58:57+03:00",
      "id": 353,
      "parents": [352],
      "metadata": ...,
      "data": null
    }
    ```
    #### 5)
    **Request:**
    `/branch/main/snapshot/filter?descending=false&name_part=test_cal\
    &min_node_id=100&max_node_id=365&min_date=2025-05-21&max_date=2025-08-20`

    Find **oldest** snapshot with :
    - name contains `test_cal`,
    - id in range(100, 365),
    - created in range (2025-05-21, 2025-08-20)

    **Response:**
    ```json
    {
        "created_at": "2025-05-21T09:54:09+03:00",
        "id": 290,
        "parents": [289],
        "metadata": ...,
        "data": null
    }
    ```

    #### 6)
    **Request:**
    `/branch/main/snapshot/filter?descending=true&name_part=test_cal\
    &min_node_id=354&max_date=2025-08-20`

    Find latest snapshot with :
    - name contains `test_cal`,
    - id greater than or equal to 354,
    - created at less than or equal 2025-08-20

    There is no any snapshot satisfying the criteria.

    **Response:**
    ```json
    null
    ```
    """
    _, snapshots = branch.get_latest_snapshots(
        pages_filter=PageFilter(per_page=1, page=1),
        search_filter=SearchWithIdFilter(**search_filters.model_dump()),
        descending=descending,
    )
    if len(snapshots) == 0:
        return None
    return snapshots[0].dump()


@branch_router.get("/node", deprecated=True)
def get_node(
    *,
    node_id: int,
    load_type: NodeLoadType = NodeLoadType.Full,
    branch: Annotated[BranchBase, Depends(_get_branch_instance)],
) -> NodeModel:
    node = branch.get_node(node_id)
    node.load(load_type)
    return node.dump()


@branch_router.get("/node/latest", deprecated=True)
def get_latest_node(
    *,
    load_type: NodeLoadType = NodeLoadType.Full,
    branch: Annotated[BranchBase, Depends(_get_branch_instance)],
) -> NodeModel:
    node = branch.get_node()
    node.load(load_type)
    return node.dump()


@branch_router.get("/snapshots_history", summary="List snapshots history")
def get_snapshots_history(
    *,
    page_filter: Annotated[PageFilter, Depends(get_page_filter)],
    descending: Annotated[
        bool,
        Query(description="When true, order from newest to oldest."),
    ] = True,
    reverse: Annotated[
        bool,
        Query(
            deprecated=True,
            description="This field is ignored. Use `descending` instead.",
        ),
    ] = False,
    global_reverse: Annotated[
        bool,
        Query(
            deprecated=True,
            description="This field is ignored. Use `descending` instead.",
        ),
    ] = False,
    sort: Annotated[
        SortField | None,
        Query(
            description=(
                "Field to sort by: 'name' (alphabetical), 'date' (creation "
                "time), or 'status' (finished first, then skipped, pending, "
                "running, error). Default sorts by date."
            )
        ),
    ] = None,
    grouped: Annotated[
        bool,
        Query(
            description=(
                "When true, returns a nested structure with workflows "
                "containing their child nodes in an 'items' array. "
                "Workflows include aggregated statistics (outcomes, "
                "nodes_completed, etc.). When false (default), returns "
                "a flat list for backward compatibility."
            )
        ),
    ] = False,
    search_filters: Annotated[SearchFilter, Depends(get_search_filter)],
    branch: Annotated[BranchBase, Depends(_get_branch_instance)],
) -> PagedCollection[SimplifiedSnapshotWithMetadata] | PagedCollection[SnapshotHistoryItem]:
    """List snapshots history.

    Returns a paginated list of snapshots for the branch. Order is controlled
    by the `descending` flag. Optionally filter by date range, name, or node ID.
    Use the `sort` parameter to sort by name, date, or status.

    When `grouped=true`, returns a nested structure where workflows contain
    their child nodes in an 'items' array, with aggregated statistics including:
    - `type_of_execution`: "node" or "workflow"
    - `items`: nested child snapshots (for workflows)
    - `outcomes`: aggregated qubit outcomes with failure tracking
    - `nodes_completed` / `nodes_total`: node success counts
    - `qubits_completed` / `qubits_total`: qubit success counts

    ### Examples

    #### 1) Basic pagination (flat)
    **Request:** `/branch/main/snapshots_history?page=1&per_page=3`

    **Response:**

    ```json
    {
      "page": 1,
      "per_page": 3,
      "total_items": 135,
      "items": [
        {
          "created_at": "2025-08-22T12:16:42+03:00",
          "id": 367,
          "parents": [
            366
          ],
          "metadata": {
            "name": "wf_node1",
            ...,
          }
        },
        {
          "created_at": "2025-08-22T10:54:07+03:00",
          "id": 366,
          "parents": [
            365
          ],
          "metadata": {
            "name": "test_cal",
            ...,
          }
        },
        {
          "created_at": "2025-08-21T14:12:28+03:00",
          "id": 365,
          "parents": [
            364
          ],
          "metadata": {
            "name": "test_cal",
            ...,
          }
        }
      ],
      "has_next_page": true,
      "total_pages": 45
    }
    ```

    #### 2) Filter by date range
    **Request:**
    `/branch/main/snapshots_history?page=1&per_page=100&min_date=2025-08-21&max_date=2025-08-22`

    Returns snapshots created between 2025-08-21 and 2025-08-22 (inclusive).

    #### 3) Filter by exact name
    **Request:**
    `/branch/main/snapshots_history?page=1&per_page=100&name=test_cal`

    Returns only snapshots with name exactly matching "test_cal".

    #### 4) Sort by name (A-Z)
    **Request:**
    `/branch/main/snapshots_history?page=1&per_page=100&sort=name&descending=false`

    Returns snapshots sorted alphabetically by name (A to Z).

    **Note:** Cannot use both `name` (exact match) and `name_part` (substring
    match) in the same request - this will return a 400 Bad Request error.
    """
    if search_filters.name is not None and search_filters.name_part is not None:
        raise HTTPException(
            status_code=400,
            detail=(
                "Cannot use both 'name' (exact match) and 'name_part' "
                "(substring match) parameters together. Use only one."
            ),
        )

    # For grouped mode or sorting, we need to fetch all snapshots first
    # because sorting and tree-building require all items in memory.
    # Check if we need to fetch all snapshots for post-filtering
    # Tag filtering requires loading metadata, so we need to fetch all first
    has_tag_filter = search_filters.tags is not None and len(search_filters.tags) > 0

    # For grouped mode, sorting, or tag filtering, we need to fetch all snapshots
    if grouped or sort is not None or has_tag_filter:
        all_pages_filter = PageFilter(page=1, per_page=MAX_PAGE_SIZE_FOR_GROUPING)
        _, all_snapshots = branch.get_latest_snapshots(
            pages_filter=all_pages_filter,
            search_filter=SearchWithIdFilter(**search_filters.model_dump()),
            descending=False,  # We'll handle sorting/pagination ourselves
            include_outcomes=True,  # Load outcomes for accurate qubit counting
        )
        all_dumped = []
        for snapshot in all_snapshots:
            dumped = snapshot.dump()
            # Extract outcomes from data if available for workflow aggregation
            outcomes = None
            if dumped.data and dumped.data.outcomes:
                outcomes = dumped.data.outcomes
            all_dumped.append(
                SimplifiedSnapshotWithMetadataAndOutcomes(
                    **dumped.model_dump(exclude={"data"}),
                    outcomes=outcomes,
                )
            )

        # Apply tag filtering if specified
        if has_tag_filter:
            all_dumped = cast(
                list[SimplifiedSnapshotWithMetadataAndOutcomes],
                _filter_snapshots_by_tags(
                    cast(list[SimplifiedSnapshotWithMetadata], all_dumped),
                    search_filters.tags or [],
                ),
            )

        if sort is not None:
            # Sort all snapshots by the requested field
            all_dumped = sorted(
                all_dumped,
                key=lambda s: get_sort_key(s, sort, descending),
                reverse=descending,
            )
        elif descending:
            # Apply descending order if not sorting
            all_dumped = list(reversed(all_dumped))

        if grouped:
            # Build nested tree structure
            tree_items = build_snapshot_tree(cast(list[SimplifiedSnapshotWithMetadata], all_dumped))

            # Sort tree items using the same sort logic as the request parameter.
            # This fixes a bug where tree_items were always sorted by created_at
            # regardless of the user's sort parameter.
            if sort is not None:
                tree_items.sort(
                    key=lambda x: get_sort_key(
                        SimplifiedSnapshotWithMetadata(
                            id=x.id,
                            created_at=x.created_at,
                            parents=x.parents,
                            metadata=x.metadata,
                        ),
                        sort,
                        descending,
                    ),
                    reverse=descending,
                )
            else:
                # Default: sort by created_at (date)
                tree_items.sort(key=lambda x: x.created_at or x.id, reverse=descending)

            # Paginate the nested items (counting all items including nested)
            paginated_items, total = paginate_nested_items(tree_items, page_filter.page, page_filter.per_page)

            return PagedCollection[SnapshotHistoryItem](
                page=page_filter.page,
                per_page=page_filter.per_page,
                total_items=total,
                items=paginated_items,
            )
        else:
            # Flat mode with sorting - apply pagination
            snapshots_dumped = list(get_page_slice(all_dumped, page_filter))
            total = len(all_dumped)

            return PagedCollection[SimplifiedSnapshotWithMetadata](
                page=page_filter.page,
                per_page=page_filter.per_page,
                total_items=total,
                items=snapshots_dumped,
            )
    else:
        # Original flow without sorting or grouping
        total, snapshots = branch.get_latest_snapshots(
            pages_filter=page_filter,
            search_filter=SearchWithIdFilter(**search_filters.model_dump()),
            descending=descending,
        )
        simple_snapshots = [SimplifiedSnapshotWithMetadata(**snapshot.dump().model_dump()) for snapshot in snapshots]

        return PagedCollection[SimplifiedSnapshotWithMetadata](
            page=page_filter.page,
            per_page=page_filter.per_page,
            total_items=total,
            items=simple_snapshots,
        )


@branch_router.get(
    "/snapshots/filter",
    summary="Paged filtered list of snapshots",
)
def get_snapshots_filtered(
    *,
    page_filter: Annotated[PageFilter, Depends(get_page_filter)],
    descending: Annotated[
        bool,
        Query(description="When true, order from newest to oldest."),
    ] = True,
    search_filters: Annotated[SearchFilter, Depends(get_search_filter)],
    branch: Annotated[BranchBase, Depends(_get_branch_instance)],
) -> PagedCollection[SimplifiedSnapshotWithMetadata]:
    """
    Retrieve the list of newest (or oldest) snapshots that satisfies the
    provided criteria. Order is controlled by the `descending` flag.
    Returns a paginated list of snapshots for the branch.

    ### Examples
    More filter examples can be found for
    `/branch/<branch_name>/snapshot/filter`

    #### 1)
    **Request:**
    `/branch/main/snapshots/filter?descending=true&page=1\
&per_page=2&name_part=test_cal`

    Find snapshots with name contains `test_cal`. Return 2 latest snapshots
    satisfying the criteria.

    **Response:**
    ```json
    {
      "page": 1,
      "per_page": 2,
      "total_items": 135,
      "items": [
        {
          "created_at": "2025-08-22T10:54:07+03:00",
          "id": 366,
          "parents": [365],
          "metadata": {
            "name": "test_cal",
            ...,
          }
        },
        {
          "created_at": "2025-08-21T14:12:28+03:00",
          "id": 365,
          "parents": [364],
          "metadata": {
            "name": "test_cal",
            ...,
          }
        }
      ],
      "has_next_page": true,
      "total_pages": 68
    }
    ```

    #### 2)
    **Request:**
    `/branch/main/snapshots/filter?descending=true&page=5\
&per_page=2&name_part=test_cal`

    Find snapshots with name contains `test_cal`. Return 2 snapshots with
    skipping 8 ((page number - 1) * per_page) snapshots. Ordered with
    descending created at.

    **Response:**
    ```json
    {
        "page": 5,
        "per_page": 2,
        "total_items": 135,
        "items": [
        {
          "created_at": "2025-08-21T13:59:05+03:00",
          "id": 358,
          "parents": [
            357
          ],
          "metadata": {
            "name": "test_cal",
            ...,
          }
        },
        {
          "created_at": "2025-08-21T12:35:53+03:00",
          "id": 357,
          "parents": [
            356
          ],
          "metadata": {
            "name": "test_cal",
            ...,
          }
        }
      ],
      "has_next_page": true,
      "total_pages": 68
    }
    ```
    """
    total, snapshots = branch.get_latest_snapshots(
        pages_filter=page_filter,
        search_filter=SearchWithIdFilter(**search_filters.model_dump()),
        descending=descending,
    )
    snapshots_dumped = [SimplifiedSnapshotWithMetadata(**snapshot.dump().model_dump()) for snapshot in snapshots]
    return PagedCollection[SimplifiedSnapshotWithMetadata](
        page=page_filter.page,
        per_page=page_filter.per_page,
        total_items=total,
        items=snapshots_dumped,
    )


@branch_router.get("/nodes_history", deprecated=True)
def get_nodes_history(
    *,
    page_filter: Annotated[PageFilter, Depends(get_page_filter)],
    descending: bool = True,
    reverse: Annotated[
        bool,
        Query(
            deprecated=True,
            description="This field is ignored. Use `descending` instead.",
        ),
    ] = False,
    global_reverse: Annotated[
        bool,
        Query(
            deprecated=True,
            description="This field is ignored. Use `descending` instead.",
        ),
    ] = False,
    branch: Annotated[BranchBase, Depends(_get_branch_instance)],
) -> PagedCollection[NodeModel]:
    total, nodes = branch.get_latest_nodes(
        pages_filter=page_filter,
        descending=descending,
    )
    nodes_dumped = [node.dump() for node in nodes]
    return PagedCollection[NodeModel](
        page=page_filter.page,
        per_page=page_filter.per_page,
        total_items=total,
        items=nodes_dumped,
    )


@branch_router.get(
    "/snapshots/search",
    summary="Search values within snapshots",
)
def search_snapshots_data(
    *,
    data_path: Annotated[Sequence[str | int], Depends(get_search_path)],
    filter_no_change: Annotated[
        bool,
        Query(description=("Exclude consecutive entries where the value did not change.")),
    ] = True,
    page_filter: Annotated[PageFilter, Depends(get_page_filter)],
    descending: Annotated[
        bool,
        Query(description="When true, order results from newest to oldest."),
    ] = True,
    search_filters: Annotated[SearchFilter, Depends(get_search_filter)],
    branch: Annotated[BranchBase, Depends(_get_branch_instance)],
) -> PagedCollection[SnapshotSearchResult]:
    """
    Search for values in quam state along a passed `data_path` across
    matching snapshots. Returning a paginated result.

    ### Examples
    #### 1)

    **Request:**
    `/branch/main/snapshots/search?filter_no_change=false&descending=true\
    &data_path=channels.ch1.intermediate_frequency&page=1&per_page=3`

    Get quam values by path `channels.ch1.intermediate_frequency` from 3 latest
    snapshots. Compare with next example for check impact of `filter_no_change`.

    **Response:**
    ```json
    {
      "page": 1,
      "per_page": 3,
      "total_items": 0,
      "items": [
        {
          "key": null,
          "value": null,
          "snapshot": {
            "created_at": "2025-08-22T12:16:42+03:00",
            "id": 367,
            "parents": [
              366
            ]
          }
        },
        {
          "key": [
            "channels",
            "ch1",
            "intermediate_frequency"
          ],
          "value": 100000000,
          "snapshot": {
            "created_at": "2025-08-22T10:54:07+03:00",
            "id": 366,
            "parents": [
              365
            ]
          }
        },
        {
          "key": [
            "channels",
            "ch1",
            "intermediate_frequency"
          ],
          "value": 100000000,
          "snapshot": {
            "created_at": "2025-08-21T14:12:28+03:00",
            "id": 365,
            "parents": [
              364
            ]
          }
        }
      ],
      "has_next_page": true,
      "total_pages": 0
    }
    ```

    #### 2)
    **Request:**
    `/branch/main/snapshots/search?filter_no_change=true&descending=true\
    &data_path=channels.ch1.intermediate_frequency&page=1&per_page=3`

    Get quam values by path `channels.ch1.intermediate_frequency` from 3 latest
    snapshots with filtering if the value wasn't changed. Compare with previous
    example for check impact of `filter_no_change`.

    **Response:**
    ```json
    {
      "page": 1,
      "per_page": 3,
      "total_items": 0,
      "items": [
        {
          "key": null,
          "value": null,
          "snapshot": {
            "created_at": "2025-08-22T12:16:42+03:00",
            "id": 367,
            "parents": [
              366
            ]
          }
        },
        {
          "key": [
            "channels",
            "ch1",
            "intermediate_frequency"
          ],
          "value": 100000000,
          "snapshot": {
            "created_at": "2025-08-21T14:05:06+03:00",
            "id": 361,
            "parents": [
              360
            ]
          }
        },
        {
          "key": [
            "channels",
            "ch1",
            "intermediate_frequency"
          ],
          "value": 50000000,
          "snapshot": {
            "created_at": "2025-08-21T14:04:14+03:00",
            "id": 360,
            "parents": [
              359
            ]
          }
        }
      ],
      "has_next_page": true,
      "total_pages": 0
    }
    ```
    """
    total, seq = branch.search_snapshots_data(
        data_path=data_path,
        filter_no_change=filter_no_change,
        pages_filter=page_filter,
        search_filter=SearchWithIdFilter(**search_filters.model_dump()),
        descending=descending,
    )

    return PagedCollection[SnapshotSearchResult](
        page=page_filter.page,
        per_page=page_filter.per_page,
        total_items=total,
        items=list(seq),
    )
