"""Department-scoped filtering helpers for skills and KB sources (U4).

These helpers take a list of "all known" skill names / KB source ids
and the user's department ids (from
:class:`agentkit.server.admin.context.DepartmentContext`) and return
the subset that the user is allowed to see.

Visibility rules (from the plan):

- A resource bound to **any** of the user's departments is visible.
- A resource bound to **no** department is *global* — visible to
  everyone (including unauthenticated callers).
- A resource bound to **other** departments only is hidden.

Admin users bypass filtering entirely — the caller is expected to
short-circuit before calling these helpers when
``DepartmentContext.is_admin`` is ``True``.

Both helpers are pure async functions (no FastAPI dependency) so they
can be unit-tested in isolation and reused from non-route contexts
(e.g. the CLI admin commands in U8).
"""

from __future__ import annotations

import logging
from pathlib import Path

import aiosqlite

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Skills
# ---------------------------------------------------------------------------


async def filter_skills_by_department(
    db_path: Path,
    department_ids: list[str],
    all_skill_names: list[str],
) -> list[str]:
    """Return the subset of ``all_skill_names`` visible to the user.

    Args:
        db_path: Path to the auth SQLite DB.
        department_ids: The user's department ids (union). Empty for
            users with no department assignments — they see only global
            skills.
        all_skill_names: All known skill names (typically from
            ``skill_registry.list_skills()``).

    Returns:
        Sorted list of skill names the user is allowed to see:
        global skills (no department binding) + skills bound to one of
        the user's departments.

    The returned list is sorted alphabetically for deterministic
    output. The order of ``all_skill_names`` is NOT preserved.
    """
    if not all_skill_names:
        return []

    async with aiosqlite.connect(str(db_path)) as db:
        # 1. Skills explicitly bound to one of the user's departments.
        bound_skills: set[str] = set()
        if department_ids:
            # Build a parameterized IN (?, ?, ...) clause.
            placeholders = ",".join("?" for _ in department_ids)
            cursor = await db.execute(
                f"SELECT DISTINCT skill_name FROM department_skill_bindings "
                f"WHERE department_id IN ({placeholders})",
                tuple(department_ids),
            )
            for row in await cursor.fetchall():
                bound_skills.add(row[0])

        # 2. Global skills — those with NO row in
        #    department_skill_bindings. We compute this as the set
        #    difference (all_known_skills - any_bound_skill) so we
        #    don't need a separate query per skill.
        cursor = await db.execute(
            "SELECT DISTINCT skill_name FROM department_skill_bindings"
        )
        any_bound_skills = {row[0] for row in await cursor.fetchall()}

    all_known = set(all_skill_names)
    global_skills = all_known - any_bound_skills

    # The user sees: their bound skills (intersected with all_known, in
    # case a binding references a skill that's no longer registered) +
    # the global skills.
    visible = (bound_skills & all_known) | global_skills
    return sorted(visible)


# ---------------------------------------------------------------------------
# KB sources
# ---------------------------------------------------------------------------


async def filter_kb_sources_by_department(
    db_path: Path,
    department_ids: list[str],
    all_kb_source_ids: list[str],
) -> list[str]:
    """Return the subset of ``all_kb_source_ids`` visible to the user.

    Same visibility rules as :func:`filter_skills_by_department` but
    for KB sources (``department_kb_bindings`` table).

    Args:
        db_path: Path to the auth SQLite DB.
        department_ids: The user's department ids (union). Empty for
            users with no department assignments.
        all_kb_source_ids: All known KB source ids (typically from
            ``KnowledgeSourceStore.list_sources()``).

    Returns:
        Sorted list of KB source ids the user is allowed to see.
    """
    if not all_kb_source_ids:
        return []

    async with aiosqlite.connect(str(db_path)) as db:
        bound_sources: set[str] = set()
        if department_ids:
            placeholders = ",".join("?" for _ in department_ids)
            cursor = await db.execute(
                f"SELECT DISTINCT kb_source_id FROM department_kb_bindings "
                f"WHERE department_id IN ({placeholders})",
                tuple(department_ids),
            )
            for row in await cursor.fetchall():
                bound_sources.add(row[0])

        cursor = await db.execute(
            "SELECT DISTINCT kb_source_id FROM department_kb_bindings"
        )
        any_bound_sources = {row[0] for row in await cursor.fetchall()}

    all_known = set(all_kb_source_ids)
    global_sources = all_known - any_bound_sources

    visible = (bound_sources & all_known) | global_sources
    return sorted(visible)
