# Validate actual final block geometry, physical interfaces, and traversable interiors.
from __future__ import annotations

from collections import deque
from functools import lru_cache
import hashlib
import json
from .geometry import STATES, DIRECTIONS, name_of


@lru_cache(None)
def parse(state):
    name = name_of(state)
    props = {}
    if state and '[' in state:
        props = dict(item.split('=') for item in state.split('[', 1)[1][:-1].split(','))
    return name, props


def walk_passable(name):
    return name in ('air', 'ladder', 'vine') or name.endswith('_door')


def solid_support(name):
    return name != 'air' and name not in ('ladder', 'vine', 'lantern', 'chain', 'iron_bars', 'campfire', 'fern', 'short_grass') and not name.endswith(('_fence', '_wall', '_door', '_trapdoor', '_slab', '_leaves', '_bed')) and not name.startswith('potted_')


def reachable(scene):
    """Flood walkable standing cells using stair orientation and supported ladders.

    Args:
        scene (Scene): Final geometry after every decorative pass.

    Returns:
        set: Reachable feet coordinates. Doors are player-operable, closed by default.
    """
    blocks = scene.blocks

    def at(x, y, z):
        return name_of(blocks.get((x, y, z)))

    candidates = set()
    for (x, y, z), state in blocks.items():
        n = name_of(state)
        if solid_support(n) or n.endswith('_stairs'):
            candidates.add((x, y + 1, z))
        if n == 'ladder':
            candidates.add((x, y, z))
            candidates.add((x, y + 1, z))
    nodes = {p for p in candidates if walk_passable(at(*p)) and walk_passable(at(p[0], p[1] + 1, p[2]))}
    start = scene.plan.approach[0]
    first = (start['x'], start['y'] + 1, start['z'])
    assert first in nodes, f'exterior start obstructed {first}: {at(*first)}'
    seen = {first}
    queue = deque([first])
    while queue:
        x, y, z = queue.popleft()
        for face, (dx, dz) in DIRECTIONS.items():
            for dy in (0, 1, -1):
                q = (x + dx, y + dy, z + dz)
                if q not in nodes or q in seen:
                    continue
                if dy:
                    stairp = (q[0], q[1] - 1, q[2]) if dy > 0 else (x, y - 1, z)
                    n, props = parse(blocks.get(stairp))
                    required = face if dy > 0 else next(k for k, d in DIRECTIONS.items() if d == (-dx, -dz))
                    if not n.endswith('_stairs') or props.get('facing') != required or props.get('half') != 'bottom':
                        continue
                seen.add(q)
                queue.append(q)
        for dy in (-1, 1):
            q = (x, y + dy, z)
            if q in nodes and q not in seen and (at(x, y, z) == 'ladder' or at(*q) == 'ladder'):
                seen.add(q)
                queue.append(q)
    return seen


def envelope_leaks(scene):
    """Flood exterior air through porous blocks and detect exposed room targets."""
    # Occupied room components share a closed envelope; report the first leaking target.
    ox, _, oz = scene.plan.origin
    sx, sy, sz = scene.plan.size_xyz
    xmin, xmax, zmin, zmax = -ox, sx - ox - 1, -oz, sz - oz - 1
    porous = {'air', 'ladder', 'vine', 'lantern', 'chain', 'iron_bars', 'campfire', 'fern', 'short_grass', 'flower_pot', 'lightning_rod'}
    barriers = set()
    for p, state in scene.blocks.items():
        n = name_of(state)
        if n in porous or n.endswith(('_stairs', '_slab', '_fence', '_wall', '_leaves', '_bed')) or n.startswith('potted_'):
            continue
        barriers.add(p)
    # Flood from every boundary face, with a compact flat integer visited set.
    plane = sx * sz

    def idx(x, y, z):
        return y * plane + (z + oz) * sx + x + ox

    blocked = bytearray(sx * sy * sz)
    for x, y, z in barriers:
        if xmin <= x <= xmax and zmin <= z <= zmax and 0 <= y < sy:
            blocked[idx(x, y, z)] = 1
    seen = bytearray(len(blocked))
    queue = deque()

    def add(i):
        if not blocked[i] and not seen[i]:
            seen[i] = 1
            queue.append(i)

    for y in range(sy):
        for z in range(sz):
            for x in (0, sx - 1):
                add(y * plane + z * sx + x)
        for x in range(sx):
            for z in (0, sz - 1):
                add(y * plane + z * sx + x)
    for z in range(sz):
        for x in range(sx):
            add(z * sx + x)
            add((sy - 1) * plane + z * sx + x)
    while queue:
        i = queue.popleft()
        y, rem = divmod(i, plane)
        z, x = divmod(rem, sx)
        if x:
            add(i - 1)
        if x < sx - 1:
            add(i + 1)
        if z:
            add(i - sx)
        if z < sz - 1:
            add(i + sx)
        if y:
            add(i - plane)
        if y < sy - 1:
            add(i + plane)
    return [p for p in scene.enclosed if seen[idx(*p)]]


def isolated_floor_cells(scene, visited):
    """Find usable interior floor cells that final furniture or framing isolated."""
    missed = set()
    for v in scene.plan.volumes:
        for f in v.levels:
            for x in range(v.x0 + 1, v.x1):
                for z in range(v.z0 + 1, v.z1):
                    if solid_support(scene.get(x, f, z)) and all(walk_passable(scene.get(x, f + dy, z)) for dy in (1, 2)) and (x, f + 1, z) not in visited:
                        missed.add((x, f + 1, z))
    return missed


def geometry_signature(scene):
    """Hash actual roof occupancy and room partitions, ignoring material noise.

    Args:
        scene (Scene): Final placed or reloaded geometry.

    Returns:
        str: Observed sample signature, not a count of possible designs.
    """
    main = scene.plan.volumes[0]
    mask = set()
    for (x, y, z), state in scene.blocks.items():
        if 'deepslate' in name_of(state):
            mask.add((x - main.x0, y - main.base, z - main.z0, 'roof'))
    for volume in scene.plan.volumes:
        for floor in volume.levels:
            for x in range(volume.x0, volume.x1 + 1):
                for z in range(volume.z0, volume.z1 + 1):
                    # Floor, wall and void geometry only. Decoration colour is irrelevant.
                    if solid_support(scene.get(x, floor, z)):
                        mask.add((x - main.x0, floor - main.base, z - main.z0, 'floor'))
                    if not walk_passable(scene.get(x, floor + 1, z)):
                        mask.add((x - main.x0, floor + 1 - main.base, z - main.z0, 'occupied'))
    return hashlib.sha256(json.dumps(sorted(mask), separators=(',', ':')).encode()).hexdigest()


def validate_scene(scene, envelope=True):
    """Check final geometry, without trusting the plan's declared room graph."""
    blocks = scene.blocks
    get = scene.get
    p = scene.plan
    ox, oy, oz = p.origin
    sx, sy, sz = p.size_xyz
    assert all(0 <= x + ox < sx and 0 <= y + oy < sy and 0 <= z + oz < sz for x, y, z in blocks), 'bounds violation'
    for state in set(blocks.values()):
        n, props = parse(state)
        assert n in STATES, state
        domains, defaults = STATES[n]
        assert set(props) == set(defaults), state
        assert all(value in domains[key] for key, value in props.items()), state
    doors, beds = 0, 0
    for (x, y, z), state in blocks.items():
        n, props = parse(state)
        if n.endswith('_door'):
            other = (x, y + (1 if props['half'] == 'lower' else -1), z)
            nn, pp = parse(blocks.get(other))
            assert n == nn and pp.get('half') != props['half'] and all(props[k] == pp[k] for k in ('facing', 'hinge', 'open', 'powered')), f'broken door {(x,y,z)}'
            if props['half'] == 'lower':
                assert solid_support(get(x, y - 1, z)), f'unsupported door {(x,y,z)}'
                doors += 1
        if n.endswith('_bed'):
            dx, dz = DIRECTIONS[props['facing']]
            sign = 1 if props['part'] == 'foot' else -1
            nn, pp = parse(blocks.get((x + sign * dx, y, z + sign * dz)))
            assert nn == n and pp.get('part') != props['part'] and pp.get('facing') == props['facing'], f'broken bed {(x,y,z)}'
            assert solid_support(get(x, y - 1, z)), f'unsupported bed {(x,y,z)}'
            beds += props['part'] == 'foot'
        if n == 'ladder':
            dx, dz = DIRECTIONS[props['facing']]
            assert solid_support(get(x - dx, y, z - dz)), f'unsupported ladder {(x,y,z)}'
        if n == 'lantern':
            dy = 1 if props['hanging'] == 'true' else -1
            assert get(x, y + dy, z) not in ('air', 'vine', 'glass'), f'unsupported lantern {(x,y,z)}'
        if n.startswith('potted_') or n in ('fern', 'short_grass', 'poppy', 'allium', 'oxeye_daisy', 'azure_bluet'):
            assert solid_support(get(x, y - 1, z)), f'unsupported plant {(x,y,z)}'
        if n == 'vine':
            assert any(props.get(face) == 'true' and solid_support(get(x + dx, y, z + dz)) for face, (dx, dz) in DIRECTIONS.items()), f'unattached vine {(x,y,z)}'
    for q, support in scene.attachments:
        assert get(*q) != 'air' and get(*support) not in ('air', 'vine'), f'detached decoration {q} -> {support}'
    for window in scene.windows:
        assert all('glass' in get(*q) for q in window['cells']), f'overwritten window {window}'
        assert all(get(*q) == 'air' for q in window['inside'] + window['outside']), f'blocked window {window}'
    for x, y, z, facing in scene.stairs:
        n, props = parse(blocks.get((x, y, z)))
        assert n.endswith('_stairs') and props['facing'] == facing, f'overwritten tread {(x,y,z)}'
        assert solid_support(get(x, y - 1, z)), f'unsupported tread {(x,y,z)}'
        assert all(walk_passable(get(x, y + dy, z)) for dy in (1, 2, 3)), f'stair headroom {(x,y,z)}'
    visited = reachable(scene)
    for room in scene.rooms:
        assert room['target'] in visited, f'unreachable {room["volume"]}/{room["purpose"]}: {room["target"]}'
        assert room['furniture'], f'unfurnished room {room}'
        assert room['lights'], f'unlit room {room}'
        assert all(get(*q) == 'lantern' for q in room['lights']), f'overwritten room lights {room}'
        assert all(abs(q[1] - room['floor']) <= 3 for q in room['lights']), f'light too high {room}'
    missed = isolated_floor_cells(scene, visited)
    assert not missed, f'isolated usable floor cells: {sorted(missed)[:12]}'
    for x, y, z in scene.doors:
        n, props = parse(blocks.get((x, y, z)))
        assert n.endswith('_door'), f'overwritten door {(x, y, z)}'
        dx, dz = DIRECTIONS[props['facing']]
        for sign in (-1, 1):
            assert all(walk_passable(get(x + sign * dx, y + dy, z + sign * dz)) for dy in (0, 1)), f'blocked doorway {(x, y, z)}'
    for v in p.volumes:
        if v.kind != 'dormer':
            assert all(solid_support(get(x, y, z)) for x in range(v.x0, v.x1 + 1) for z in range(v.z0, v.z1 + 1) for y in range(v.base)), f'discontinuous foundation {v.name}'
    assert beds >= 1, 'no sleeping accommodation'
    if envelope:
        leaks = envelope_leaks(scene)
        assert not leaks, f'weather envelope leaks to {leaks}'
    return {'rooms': len(scene.rooms), 'beds': beds, 'doors': doors, 'windows': len(scene.windows), 'walking_stairs': len(scene.stairs), 'reachable_cells': len(visited), 'blocks': sum(name_of(s) != 'air' for s in blocks.values()), 'envelope_checked': envelope}
