# 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

    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 contact_footprint(self):
        return tuple(sorted(set(self.footprint) | ({(x, z) for x in range(self.bay) for z in range(-3, 0)} if self.porch else set())))

    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')
        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')


@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 + (3 if self.design.porch else 0)
        box = Box((-1, min(minimum - self.site_limits.bed_thickness + 1, 0), -front), (width, floor + height + b + 12, depth))
        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}
        approach = Approach(b // 2, floor, 3 if self.design.porch else 0, self.site_limits).negotiate(ctx, {})
        access = approach.ports[0]
        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}
        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) == (0, 0, 0)))
            windows = tuple(side for side, q in adjacent.items() if q not in cells)
            key = f'room-{r.x}-{r.z}-{r.level}'
            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)
            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), frame=Frame((0, floor, 0))))
        p.children.append(Child('approach', Approach(b // 2, floor, 3 if self.design.porch else 0, self.site_limits), bindings=(Binding(self.terrain, 'construction', 'surface', ctx.frame.box(contract.envelope)),)))
        # Rectangular roof runs are derived from exposed top rooms, not a building family.
        top = {(r.x, r.z, r.level) for r in rooms if (r.x, r.z, r.level + 1) not in cells}
        groups = []
        while top:
            x, z, l = min(top, key=lambda v: (v[2], v[1], v[0]))
            axis = self.design.roof_axis
            if axis == 'auto':
                axis = 'x' if (x + 1, z, l) in top else 'z'
            run = [(x, z, l)]
            while ((run[-1][0] + (axis == 'x'), run[-1][1] + (axis == 'z'), l)) in top:
                run.append((run[-1][0] + (axis == 'x'), run[-1][1] + (axis == 'z'), l))
            top -= set(run)
            groups.append((x, z, l, axis, len(run)))
        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)
            p.children.append(Child(f'roof-{i}', Roof(w, d, axis, self.design.roof_material, self.design.steep), 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)
