# Room-graph building composition and shared facade/roof planning.
from dataclasses import dataclass
from hearth.space import Box, Frame
from hearth.kernel import Capability, Contract, Domain, Port, Plan, Child, Choice, Binding, Rule, ContractError
from hearth.adapters.site import survey, Foundation, Approach, SiteLimits
from .interior import Room
from .roofs import Roof, Porch


@dataclass(frozen=True)
class RoomSpec:
    x: int
    z: int
    level: int
    purpose: str


@dataclass(frozen=True)
class Blueprint:
    rooms: tuple[RoomSpec, ...]
    bay: int = 11
    porch: bool = True
    roof_axis: str = 'auto'
    roof_material: str = 'deepslate_tile'
    steep: bool = True
    window_component: object = None
    light_component: object = None
    roof_design: object = None
    porch_depth: int = 3
    balcony: bool = False
    chimney: bool = False

    def normalized(self):
        return tuple(sorted(self.rooms, key=lambda r: (r.level, r.z, r.x, r.purpose)))

    @property
    def footprint(self):
        return tuple(sorted((r.x * self.bay + x, r.z * self.bay + z) for r in self.rooms if r.level == 0 for x in range(self.bay) for z in range(self.bay)))

    @property
    def chimney_anchor(self):
        back = max(r.z for r in self.rooms if r.level == 0)
        column = min(r.x for r in self.rooms if r.level == 0 and r.z == back)
        return (column * self.bay + self.bay // 2 - 1, (back + 1) * self.bay)

    @property
    def contact_footprint(self):
        cells = set(self.footprint)
        if self.porch:
            cells |= {(x, z) for x in range(self.bay) for z in range(-self.porch_depth, 0)}
        if self.chimney:
            cx, cz = self.chimney_anchor
            cells |= {(cx + x, cz + z) for x in range(3) for z in range(2)}
        return tuple(sorted(cells))

    def validate(self):
        cells = {(r.x, r.z, r.level) for r in self.rooms}
        if len(cells) != len(self.rooms) or (0, 0, 0) not in cells or min(min(r.x, r.z, r.level) for r in self.rooms) < 0:
            raise ValueError('Unique nonnegative rooms and entrance room (0,0,0) required')
        if self.bay not in (9, 11, 13):
            raise ValueError('Supported room bays: 9, 11, 13')
        if not 3 <= self.porch_depth <= 5:
            raise ValueError('Porch depth must be 3..5')
        if self.balcony and (not self.porch or (0, 0, 1) not in cells):
            raise ValueError('Balcony requires porch and front upper room')
        for x, z, level in cells:
            if level and (x, z, level - 1) not in cells:
                raise ValueError('Upper rooms require a supporting lower room')
        reached = {(0, 0, 0)}
        while True:
            more = {p for p in cells if any(sum(abs(a - b) for a, b in zip(p, q)) == 1 for q in reached)}
            if more <= reached:
                break
            reached |= more
        if reached != cells:
            raise ValueError('Room adjacency graph disconnected')


def roof_runs(rooms, preferred_axis='auto'):
    """Partition exposed room tops into contiguous runs with explicit ridge axes."""
    cells = {(r.x, r.z, r.level) for r in rooms}
    top = {q for q in cells if (q[0], q[1], q[2] + 1) not in cells}
    groups = []
    while top:
        x, z, level = min(top, key=lambda v: (v[2], v[1], v[0]))
        axis = preferred_axis
        if axis == 'auto':
            axis = 'x' if (x + 1, z, level) in top else 'z'
        run = [(x, z, level)]
        while (run[-1][0] + (axis == 'x'), run[-1][1] + (axis == 'z'), level) in top:
            run.append((run[-1][0] + (axis == 'x'), run[-1][1] + (axis == 'z'), level))
        top -= set(run)
        groups.append((x, z, level, axis, len(run)))
    return tuple(groups)


@dataclass(frozen=True)
class Building:
    design: Blueprint
    terrain: str
    site_limits: SiteLimits = SiteLimits()
    support_strategy: str | None = None

    def capability(self):
        return Capability('building.rooms', ('building', 'habitable', 'composite'), offers=('entrance', 'street', 'attachment'), assumptions=('known ground', 'bounded relief'), adaptations=('foundation', 'approach', 'roof junction'), guarantees=('locked room graph', 'internal reachability', 'furnishing', 'lighting'), inputs={'design.bay': Domain(choices=(9, 11, 13)), 'room_graph': 'connected integer grid with supported levels', 'relief_limit': self.site_limits.relief, 'support_height_limit': self.site_limits.support_height, 'frame.turn': Domain(choices=(0, 1, 2, 3))}, locked=('design.rooms', 'design.bay', 'design.roof_axis', 'design.porch', 'frame.x', 'frame.z'))

    def negotiate(self, ctx, parameters):
        self.design.validate()
        b = self.design.bay
        rooms = self.design.normalized()
        measured = survey(ctx, self.design.contact_footprint, self.site_limits)
        if self.support_strategy is not None:
            if self.support_strategy not in measured['feasible_strategies']:
                raise ContractError('contact-strategy', ctx.path, ctx.frame.origin, 'Requested strategy incompatible with measured water/relief')
            measured['strategy'] = self.support_strategy
        floor = measured['floor']
        height = max(r.level for r in rooms) * 6
        width = (max(r.x for r in rooms) + 1) * b
        depth = (max(r.z for r in rooms) + 1) * b
        minimum = min(v[0] for v in measured['samples'].values())
        front = self.site_limits.approach_length + (self.design.porch_depth if self.design.porch else 0)
        box = Box((-1, min(minimum - self.site_limits.bed_thickness + 1, 0), -front), (width, floor + height + b + 16, depth + (3 if self.design.chimney else 0)))
        points = tuple((r.x * b + b // 2, floor + r.level * 6 + 1, r.z * b + b // 2) for r in rooms)
        entrance = (b // 2, floor + 1, 0)
        route = Rule('route', (entrance,) + points, box, phase='complete')
        port = Port('entrance', 'access', Frame(entrance), Box(entrance, (entrance[0], entrance[1] + 1, entrance[2])), 10)
        decisions = {'floor': floor, 'relief': measured['relief'], 'wet': measured['wet'], 'foundation': measured['strategy'], 'rooms': [(r.x, r.z, r.level, r.purpose) for r in rooms], 'bay': b, 'roof_axis': self.design.roof_axis, 'porch': self.design.porch, 'roof_material': self.design.roof_material, 'steep': self.design.steep, 'balcony': self.design.balcony, 'porch_depth': self.design.porch_depth, 'chimney': self.design.chimney}
        approach = Approach(b // 2, floor, self.design.porch_depth if self.design.porch else 0, self.site_limits).negotiate(ctx, {})
        access = approach.ports[0]
        route = Rule('route', (entrance,) + points + (access.frame.origin,), box, phase='complete')
        street = Port('street', access.kind, access.frame, access.region, access.capacity, facts={'external_connection_required': True}, delegate=('approach', 'street'))
        return Contract(box, ports=(port, street), rules=(route,), decisions=decisions, reads=(Box((0, minimum, 0), (width - 1, floor, depth - 1)),))

    def realize(self, ctx, contract):
        p = Plan()
        b = self.design.bay
        floor = contract.decisions['floor']
        rooms = self.design.normalized()
        cells = {(r.x, r.z, r.level) for r in rooms}
        groups = roof_runs(rooms, self.design.roof_axis)
        roof_axes = {(x + i * (axis == 'x'), z + i * (axis == 'z'), level): axis for x, z, level, axis, length in groups for i in range(length)}
        p.children.append(Child('foundation', Foundation(self.design.contact_footprint, floor, contract.decisions['foundation'], self.site_limits), bindings=(Binding(self.terrain, 'construction', 'fill', ctx.frame.box(contract.envelope)),)))
        # A spanning tree of vertical access: one flight per upper horizontal component.
        stair_columns = set()
        for level in sorted({r.level for r in rooms if r.level}):
            remaining = {(x, z) for x, z, l in cells if l == level}
            while remaining:
                seed = min(remaining)
                connected = {seed}
                while True:
                    expanded = {v for v in remaining if any(abs(v[0] - q[0]) + abs(v[1] - q[1]) == 1 for q in connected)}
                    if expanded <= connected:
                        break
                    connected |= expanded
                root = min(connected)
                stair_columns.add((root[0], root[1], level - 1))
                remaining -= connected
        for r in rooms:
            adjacent = {'north': (r.x, r.z - 1, r.level), 'south': (r.x, r.z + 1, r.level), 'west': (r.x - 1, r.z, r.level), 'east': (r.x + 1, r.z, r.level)}
            doors = tuple(side for side, q in adjacent.items() if q in cells or (side == 'north' and (r.x, r.z, r.level) in ({(0, 0, 0), (0, 0, 1)} if self.design.balcony else {(0, 0, 0)})))
            abutments = {side for side, (x, z, level) in adjacent.items() if level and (x, z, level - 1) in roof_axes and roof_axes[(x, z, level - 1)] == ('z' if side in ('north', 'south') else 'x')}
            windows = tuple(side for side, q in adjacent.items() if q not in cells and side not in abutments and not (self.design.chimney and side == 'south' and (r.x * b + b // 2 - 1, (r.z + 1) * b) == self.design.chimney_anchor))
            key = f'room-{r.x}-{r.z}-{r.level}'
            flush = tuple(side for side, (x, z, level) in adjacent.items() if r.level and (x, z, level - 1) in cells and (x, z, level) not in cells)
            room = Room(r.purpose, b, b, doors, windows, (r.x, r.z, r.level + 1) in cells, (r.x, r.z, r.level) in stair_columns, (r.x, r.z, r.level - 1) in stair_columns, self.design.window_component, self.design.light_component, ('north',) if self.design.porch and r.x == 0 and r.z == 0 and r.level <= 1 else (), flush)
            p.children.append(Child(key, room, frame=Frame((r.x * b, floor + r.level * 6, r.z * b))))
        if self.design.porch:
            p.children.append(Child('porch', Porch(b, self.design.porch_depth, self.design.balcony), frame=Frame((0, floor, 0)), bindings=(Binding(ctx.path + '/room-0-0-0', 'north', 'connect'),)))
        p.children.append(Child('approach', Approach(b // 2, floor, self.design.porch_depth if self.design.porch else 0, self.site_limits), bindings=(Binding(self.terrain, 'construction', 'surface', ctx.frame.box(contract.envelope)),)))
        if self.design.chimney:
            from .masonry import Chimney
            from hearth.adapters.bearing import BearingCourse
            cx, cz = self.design.chimney_anchor
            level = max(r.level for r in rooms if (r.x * b + b // 2 - 1, (r.z + 1) * b) == (cx, cz))
            peak = max(self.design.roof_design.heights(b)) if self.design.roof_design else (b + 1) // 2
            stack_height = level * 6 + 7 + peak + 4
            p.children.append(Child('chimney-bearing', BearingCourse(3, 2), frame=Frame((cx, floor - 1, cz)), bindings=(Binding(self.terrain, 'construction', 'fill'),)))
            p.children.append(Child('chimney', Chimney(stack_height), frame=Frame((cx, floor, cz))))
        # Roof and facade planners use the same public run partition.
        for i, (x, z, l, axis, length) in enumerate(groups):
            w = b * (length if axis == 'x' else 1)
            d = b * (length if axis == 'z' else 1)
            cover = self.design.roof_design.component(w, d, axis, self.design.roof_material) if self.design.roof_design else Roof(w, d, axis, self.design.roof_material, self.design.steep)
            p.children.append(Child(f'roof-{i}', cover, frame=Frame((x * b, floor + l * 6 + 7, z * b))))
        return p


def contact_quality(view, path):
    """Score actual candidate sitework plus exposed-support proportions."""
    facts = view.contract(path).decisions
    cost = view.cost(path, 'siteworks')
    exposure = 0.4 if facts['foundation'] == 'piers' and not facts['wet'] and facts['relief'] < 2 else 0
    return cost['occupied'] / 100 + cost['replaced'] / 50 + exposure


def adapted(key, design, terrain, frame=Frame(), limits=SiteLimits()):
    """Compare complete bounded support alternatives with the principal design locked."""
    candidates = tuple(Child(key, Building(design, terrain, limits, strategy), frame=frame) for strategy in ('stepped', 'piers'))
    return Choice(key, candidates, contact_quality)
