#!/usr/bin/env python3
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.
"""Validate that every directory containing a `pyproject.toml` under
`tools/` is registered as a uv-workspace member in the root
`pyproject.toml`'s `[tool.uv.workspace] members` list.

This is the safety net for the DRY refactor introduced by the
uv-workspace adoption: the workspace members list is the single
source of truth for which projects get pre-commit hooks + CI matrix
entries. A new `tools/<name>/pyproject.toml` that is *not* added to
the members list silently skips both surfaces — exactly the bug
this hook prevents.

Run as a prek hook on every pyproject.toml change. Exit code 0 if
the two sets agree; 1 otherwise, with a diff explaining what to add
or remove.

Scope: only `tools/*/pyproject.toml` and `tools/*/*/pyproject.toml`
(maxdepth-3). The root `pyproject.toml` and any deeper nested
pyprojects (e.g. inside `tests/` fixtures or vendored deps) are
excluded.
"""

from __future__ import annotations

import sys
import tomllib
from pathlib import Path

ROOT = Path(__file__).resolve().parents[2]


def find_member_dirs() -> list[Path]:
    """Discover every directory under `tools/` that contains its own
    `pyproject.toml`, at depth 2 or 3 below the repo root."""
    found: list[Path] = []
    tools = ROOT / "tools"
    if not tools.is_dir():
        return found
    for top in sorted(tools.iterdir()):
        if not top.is_dir():
            continue
        if (top / "pyproject.toml").is_file():
            found.append(top)
            # When a top-level tools/<name> is itself a project, do
            # NOT descend further — its `tests/` and `src/` are
            # internal layout, not nested members.
            continue
        for child in sorted(top.iterdir()):
            if not child.is_dir():
                continue
            if (child / "pyproject.toml").is_file():
                found.append(child)
    return found


def read_workspace_members() -> set[str]:
    with (ROOT / "pyproject.toml").open("rb") as f:
        data = tomllib.load(f)
    try:
        return set(data["tool"]["uv"]["workspace"]["members"])
    except KeyError:
        sys.stderr.write(
            "error: root pyproject.toml has no [tool.uv.workspace] members\n"
        )
        sys.exit(2)


def main() -> int:
    member_dirs = find_member_dirs()
    declared = read_workspace_members()
    found_paths = {str(p.relative_to(ROOT)) for p in member_dirs}

    missing = sorted(found_paths - declared)
    stale = sorted(declared - found_paths)

    if not missing and not stale:
        return 0

    out = sys.stderr.write
    out("error: uv workspace members list drifts from on-disk pyprojects\n")
    out("\n")
    if missing:
        out(
            "Found `tools/.../pyproject.toml` that is NOT in "
            "`[tool.uv.workspace] members`:\n"
        )
        for p in missing:
            out(f"  + {p!r}\n")
        out("\n")
        out(
            "Add each to the `members = [...]` array in the root "
            "pyproject.toml — without this, the project will silently "
            "be skipped by the workspace-* prek hooks and the CI "
            "pytest matrix.\n\n"
        )
    if stale:
        out(
            "Workspace members list references paths that no longer "
            "have a pyproject.toml on disk:\n"
        )
        for p in stale:
            out(f"  - {p!r}\n")
        out("\n")
        out("Remove each stale entry from the root pyproject.toml.\n\n")
    return 1


if __name__ == "__main__":
    sys.exit(main())
