# Reusable geometric checks registered through the public validator extension point.
from hearth.kernel.validation import validator, state, fail
from hearth.blocks import solid, name_of
from collections import deque


@validator('vertical-cover')
def vertical_cover(scene, node, rule):
    """Check actual weather cover along bounded world-vertical columns.

    Horizontal frame rotations preserve world Y. This is a voxel coverage check,
    not a fluid simulator or a structural-mechanics proof.
    """
    for x, y, z in rule.cells:
        if not any(solid(state(scene, (x, yy, z))) or 'glass' in name_of(state(scene, (x, yy, z))) for yy in range(y, rule.box.hi[1] + 1)):
            yield fail('roof-cover', node, (x, y, z), 'Weather skin has an uncovered column')


@validator('enclosed-volume')
def enclosed_volume(scene, node, rule):
    """Flood actual air to detect exterior leaks, exempting explicit floor apertures.

    This is a six-neighbor voxel enclosure check, not fluid or collision simulation.
    Apertures are node-local cells on the bounded volume's floor boundary.
    """
    allowed = {node.frame.point(tuple(q)) for q in rule.data.get('apertures', ())}
    if any(p[1] != rule.box.lo[1] or not rule.box.contains(p) for p in allowed):
        yield fail('enclosure-aperture', node, None, 'Apertures must be bounded floor cells')
        return

    def permeable(p):
        name = name_of(state(scene, p))
        return not solid(state(scene, p)) and 'glass' not in name and not name.endswith('_door')

    reached = {q for q in rule.cells if permeable(q)}
    if not reached:
        yield fail('enclosure-seed', node, rule.cells[0] if rule.cells else None, 'The room needs an actual interior air cell')
        return
    queue = deque(sorted(reached))
    while queue:
        p = queue.popleft()
        if p in allowed:
            continue
        if any(p[k] in (rule.box.lo[k], rule.box.hi[k]) for k in range(3)):
            yield fail('enclosure-leak', node, p, 'Actual interior air reaches the exterior boundary')
            return
        for dx, dy, dz in ((1, 0, 0), (-1, 0, 0), (0, 1, 0), (0, -1, 0), (0, 0, 1), (0, 0, -1)):
            q = (p[0] + dx, p[1] + dy, p[2] + dz)
            if q not in reached and rule.box.contains(q) and permeable(q):
                reached.add(q)
                queue.append(q)
