"""Repository for the ``user_groups`` table.

A ``user_group`` is a named bucket admins create (e.g. ``data-team``,
``Engineering``) plus the two seeded ``is_system=TRUE`` groups ``Admin``
and ``Everyone``. Membership lives in
:mod:`src.repositories.user_group_members`; resource grants in
:mod:`src.repositories.resource_grants`.

System groups are write-protected — :exc:`SystemGroupProtected` is raised
on attempts to rename or delete them so the canonical ``Admin`` /
``Everyone`` names referenced from code (``app.auth.access``) cannot
disappear out from under the authorization layer.
"""

from __future__ import annotations

from datetime import datetime, timezone
from typing import Any, Dict, List, Optional
from uuid import uuid4

import duckdb


class SystemGroupProtected(Exception):
    """Raised when a mutation is attempted on a system user group (is_system=TRUE)."""


class UserGroupsRepository:
    def __init__(self, conn: duckdb.DuckDBPyConnection):
        self.conn = conn

    _SELECT_COLS = "id, name, description, is_system, created_at, created_by"

    def list_all(self) -> List[Dict[str, Any]]:
        rows = self.conn.execute(
            f"SELECT {self._SELECT_COLS} FROM user_groups ORDER BY name"
        ).fetchall()
        columns = [d[0] for d in self.conn.description]
        return [dict(zip(columns, r)) for r in rows]

    def get(self, group_id: str) -> Optional[Dict[str, Any]]:
        row = self.conn.execute(
            f"SELECT {self._SELECT_COLS} FROM user_groups WHERE id = ?",
            [group_id],
        ).fetchone()
        if not row:
            return None
        columns = [d[0] for d in self.conn.description]
        return dict(zip(columns, row))

    def get_by_name(self, name: str) -> Optional[Dict[str, Any]]:
        row = self.conn.execute(
            f"SELECT {self._SELECT_COLS} FROM user_groups WHERE name = ?",
            [name],
        ).fetchone()
        if not row:
            return None
        columns = [d[0] for d in self.conn.description]
        return dict(zip(columns, row))

    def create(
        self,
        name: str,
        description: Optional[str] = None,
        created_by: Optional[str] = None,
        is_system: bool = False,
    ) -> Dict[str, Any]:
        group_id = uuid4().hex
        self.conn.execute(
            "INSERT INTO user_groups (id, name, description, is_system, created_at, created_by) "
            "VALUES (?, ?, ?, ?, ?, ?)",
            [group_id, name, description, is_system, datetime.now(timezone.utc), created_by],
        )
        # v39: every newly-created group inherits the mandatory tier — i.e.
        # gets a resource_grants row for every plugin currently flagged
        # is_system=TRUE on marketplace_plugins. Centralised here (rather
        # than at each call site: admin POST, Google sync ensure(),
        # ensure_system seed) so all three paths stay symmetric. On fresh
        # installs the SELECT returns 0 rows so the fanout is a free
        # no-op; on running instances with existing system plugins the
        # new group is fully provisioned before its first member logs in.
        # Fail-soft: a fanout error must not block group creation, since
        # the legacy admin reconcile path (toggling each plugin manually
        # in /admin/access) still works and remains a recovery option.
        try:
            from src.repositories.resource_grants import (
                ResourceGrantsRepository,
            )
            ResourceGrantsRepository(self.conn).fanout_system_for_group(
                group_id, assigned_by=created_by,
            )
        except Exception:
            # Logger import lives at module scope to keep this hot path
            # cheap; absent (no logger configured) → swallow silently
            # rather than raise a NameError. The audit_log row written
            # by the API layer captures the group create event itself,
            # so an admin can manually fan out via /admin/access.
            try:
                import logging as _logging
                _logging.getLogger(__name__).exception(
                    "system-plugin grant fanout failed for new group %s",
                    group_id,
                )
            except Exception:
                pass
        return self.get(group_id)  # type: ignore[return-value]

    def ensure(
        self, name: str, description: Optional[str] = None
    ) -> Dict[str, Any]:
        """Idempotent get-or-create for claim-driven groups.

        Existing row is returned unchanged (preserves `is_system` and
        description — a later Google-sync call must not override an admin's
        manual description edit).
        """
        existing = self.get_by_name(name)
        if existing:
            return existing
        return self.create(
            name=name,
            description=description or "Auto-created from Google Workspace claim",
            created_by="system:google-sync",
        )

    def ensure_system(self, name: str, description: str) -> Dict[str, Any]:
        """Idempotently ensure a system group exists.

        If a group with the given name exists (manually created by an admin),
        promote it to system (is_system=TRUE). Otherwise create a new one.
        """
        existing = self.get_by_name(name)
        if existing:
            if not existing.get("is_system"):
                self.conn.execute(
                    "UPDATE user_groups SET is_system = TRUE WHERE id = ?",
                    [existing["id"]],
                )
                existing = self.get(existing["id"])  # type: ignore[assignment]
            return existing  # type: ignore[return-value]
        return self.create(name=name, description=description, is_system=True)

    def update(
        self,
        group_id: str,
        *,
        name: Optional[str] = None,
        description: Optional[str] = None,
    ) -> None:
        # Block rename of system groups — the canonical names "Admin" /
        # "Everyone" are referenced from `app.auth.access` and the
        # marketplace filter and must not move. Description edits are
        # cosmetic and allowed (admins curate them in /admin/access).
        existing = self.get(group_id)
        if (
            existing
            and existing.get("is_system")
            and name is not None
            and name != existing["name"]
        ):
            raise SystemGroupProtected(
                f"group {existing.get('name')!r} is a system group and cannot be renamed"
            )
        sets: List[str] = []
        params: List[Any] = []
        if name is not None:
            sets.append("name = ?")
            params.append(name)
        if description is not None:
            sets.append("description = ?")
            params.append(description)
        if not sets:
            return
        params.append(group_id)
        self.conn.execute(
            f"UPDATE user_groups SET {', '.join(sets)} WHERE id = ?", params
        )

    def delete(self, group_id: str) -> None:
        existing = self.get(group_id)
        if existing and existing.get("is_system"):
            raise SystemGroupProtected(
                f"group {existing.get('name')!r} is a system group and cannot be deleted"
            )
        self.conn.execute("DELETE FROM user_groups WHERE id = ?", [group_id])
