"""CRUD for `site_department` (§5.2) — the Qingyuan-specific dept_path layer.

Key invariant enforced both in the DB (CHECK) and here (defensive): when
`dept_binding == 'mapped'`, `local_dept_id` MUST be set; for the other four
values it MUST be NULL. We validate before INSERT/UPDATE so the caller gets
a clean `ValueError` instead of an opaque constraint violation.
"""
from __future__ import annotations

from typing import Any

from sqlalchemy import select
from sqlalchemy.orm import Session

from govcrawler.models import DEPT_BINDINGS, SiteDepartment


def _check_binding(dept_binding: str, local_dept_id: int | None) -> None:
    if dept_binding not in DEPT_BINDINGS:
        raise ValueError(
            f"dept_binding must be one of {DEPT_BINDINGS}, got {dept_binding!r}"
        )
    if dept_binding == "mapped" and local_dept_id is None:
        raise ValueError("dept_binding='mapped' requires local_dept_id")
    if dept_binding != "mapped" and local_dept_id is not None:
        raise ValueError(
            f"dept_binding={dept_binding!r} must have local_dept_id IS NULL"
        )


def get(session: Session, site_id: int, dept_path: str) -> SiteDepartment | None:
    return session.scalar(
        select(SiteDepartment).where(
            SiteDepartment.site_id == site_id,
            SiteDepartment.dept_path == dept_path,
        )
    )


def get_by_id(session: Session, dept_id: int) -> SiteDepartment | None:
    return session.get(SiteDepartment, dept_id)


def list_for_site(session: Session, site_id: int) -> list[SiteDepartment]:
    return list(
        session.scalars(
            select(SiteDepartment)
            .where(SiteDepartment.site_id == site_id)
            .order_by(SiteDepartment.dept_path)
        )
    )


def upsert(
    session: Session,
    *,
    site_id: int,
    dept_path: str,
    dept_binding: str = "pending",
    local_dept_id: int | None = None,
    **fields: Any,
) -> SiteDepartment:
    """Create-or-update by (site_id, dept_path). Binding invariant enforced."""
    _check_binding(dept_binding, local_dept_id)
    row = get(session, site_id, dept_path)
    if row is None:
        row = SiteDepartment(
            site_id=site_id,
            dept_path=dept_path,
            dept_binding=dept_binding,
            local_dept_id=local_dept_id,
            **fields,
        )
        session.add(row)
    else:
        row.dept_binding = dept_binding
        row.local_dept_id = local_dept_id
        for k, v in fields.items():
            setattr(row, k, v)
    return row


def rebind(
    session: Session,
    *,
    site_id: int,
    dept_path: str,
    dept_binding: str,
    local_dept_id: int | None = None,
) -> SiteDepartment:
    """Change just the binding — the common admin-UI action.

    Raises KeyError if the dept row doesn't exist yet (caller should upsert
    first) and ValueError if the (binding, local_dept_id) pair is inconsistent.
    """
    _check_binding(dept_binding, local_dept_id)
    row = get(session, site_id, dept_path)
    if row is None:
        raise KeyError(f"site_department not found: site_id={site_id} path={dept_path}")
    row.dept_binding = dept_binding
    row.local_dept_id = local_dept_id
    return row
