# Measured ribbon circulation, support grids and guard geometry through public contracts.
from dataclasses import dataclass
import math
from hearth import Box, Frame, Binding, ContractError
from hearth.blocks import state_of, stairs, slab, log, solid, passable, name_of
from hearth.kernel import Capability, Domain, Contract, Port, Grant, Plan, Rule, Child, validator, Diagnostic
from .geometry import Polyline, CARDINAL, bounds2, perimeter, lattice
from .interfaces import Endpoint


@validator('extension.guard')
def guard_rule(scene, node, rule):
    for p in rule.cells:
        if name_of(scene.view.state(p)) not in ('spruce_fence', 'spruce_log', 'stone_brick_wall'):
            yield Diagnostic('bridge-guard', node.path, p, 'A required outer guard cell is absent')


@validator('extension.span')
def span_rule(scene, node, rule):
    supports = [node.frame.point(tuple(p)) for p in rule.data['anchors']]
    for p in rule.cells:
        if not supports or min(abs(p[0] - q[0]) + abs(p[2] - q[2]) for q in supports) > rule.data['maximum']:
            yield Diagnostic('unsupported-span', node.path, p, 'Deck exceeds declared supported horizontal span')


@dataclass(frozen=True)
class Surface:
    points: tuple
    role: str = 'deck'
    material: str = 'spruce_planks'

    def capability(self):
        tags = ('surface', self.role) + (('path', 'siteworks') if self.role in ('deck', 'platform') else ())
        return Capability('extension.' + self.role, tags)

    def negotiate(self, ctx, parameters):
        box = Box.enclosing(self.points)
        return Contract(box, rules=(Rule('support', self.points),))

    def realize(self, ctx, contract):
        p = Plan()
        heights = {(x, z): y for x, y, z in self.points}
        for x, y, z in self.points:
            uphill = next((face for dx, dz, face in ((1, 0, 'east'), (-1, 0, 'west'), (0, 1, 'south'), (0, -1, 'north')) if heights.get((x + dx, z + dz), y) == y + 1), None)
            p.block((x, y, z), stairs('spruce', uphill) if uphill else state_of(self.material), 'walk-surface')
        return p


@dataclass(frozen=True)
class Trestles:
    columns: tuple
    maximum: int = 8

    def capability(self):
        return Capability('extension.trestles', ('siteworks', 'support'), adaptations=('posts to actual bed', 'stone shoes'), inputs={'maximum': Domain(2, 8)})

    def negotiate(self, ctx, parameters):
        cells = tuple((x, y, z) for x, bed, z, top, wet in self.columns for y in range(bed - 1, top + 1))
        if any(top - bed > self.maximum for x, bed, z, top, wet in self.columns):
            raise ContractError('connection-support-height', ctx.path, conditions=f'Post exceeds {self.maximum}')
        return Contract(Box.enclosing(cells), rules=(Rule('support', cells),), decisions={'columns': self.columns})

    def realize(self, ctx, contract):
        p = Plan()
        for x, bed, z, top, wet in self.columns:
            for y in range(bed + 1, top + 1):
                p.block((x, y, z), state_of('stone_bricks') if y == bed + 1 or not wet else log(), 'pier')
        return p


@dataclass(frozen=True)
class Guards:
    points: tuple
    covered: bool = False
    roof: tuple = ()

    def capability(self):
        return Capability('extension.guards', ('railing', 'roof' if self.covered else 'guard'), guarantees=('continuous outer guard',))

    def negotiate(self, ctx, parameters):
        return Contract(Box.enclosing((*self.points, *self.roof)).expand(1), rules=(Rule('extension.guard', self.points),))

    def realize(self, ctx, contract):
        p = Plan()
        occupied = {(x, z) for x, y, z in self.points}
        for i, (x, y, z) in enumerate(self.points):
            connections = {face: str((x + dx, z + dz) in occupied).lower() for dx, dz, face in ((1, 0, 'east'), (-1, 0, 'west'), (0, 1, 'south'), (0, -1, 'north'))}
            p.block((x, y, z), state_of('spruce_fence', waterlogged='false', **connections), 'guard')
            if self.covered:
                for yy in (y + 1, y + 2):
                    p.block((x, yy, z), log() if i % 5 == 0 else state_of('spruce_fence', waterlogged='false', **connections), 'gallery-frame')
        for x, y, z in self.roof:
            p.block((x, y, z), slab('dark_oak', 'top'), 'gallery-canopy')
        return p


@dataclass(frozen=True)
class Link:
    start: Endpoint
    end: Endpoint
    terrain: str
    via: tuple = ()
    curve: Polyline | None = None
    width: int = 3
    rise: int = 0
    covered: bool = False
    max_post: int = 8
    max_span: int = 6
    max_length: int = 120
    fill_limit: int = 800

    def capability(self):
        return Capability('extension.gallery' if self.covered else 'extension.bridge', ('connection', 'path', 'composite'), offers=('access',), adaptations=('bounded height profile', 'actual-bed trestles', 'supported deck reuse at endpoints'), guarantees=('both public endpoints reachable', 'two-cell headroom', 'outer guard'), inputs={'width': Domain(choices=(3, 5)), 'rise': Domain(0, 3), 'max_post': Domain(2, 8), 'max_span': Domain(3, 6)}, locked=('start', 'end', 'via', 'curve', 'width', 'covered'))

    def negotiate(self, ctx, parameters):
        pa, a, da = self.start.resolve(ctx)
        pb, b, db = self.end.resolve(ctx)
        required = {(self.start.host, self.start.port), (self.end.host, self.end.port)}
        if not required.issubset({(v.host, v.port) for v in ctx.bindings if v.verb == 'connect'}):
            raise ContractError('endpoint-binding', ctx.path, conditions='Both endpoints need explicit read-only connection bindings')
        points = ((a[0], a[2]), (a[0] + 3 * da[0], a[2] + 3 * da[1]), *self.via, (b[0] + 3 * db[0], b[2] + 3 * db[1]), (b[0], b[2]))
        curve = self.curve or Polyline(tuple(points))
        if curve.points[0] != (a[0], a[2]) or curve.points[-1] != (b[0], b[2]):
            raise ContractError('endpoint-disconnected', ctx.path, ctx.world(a), 'Curve ends must match the negotiated public ports')
        for point, next_point, direction in ((curve.points[0], curve.points[1], da), (curve.points[-1], curve.points[-2], db)):
            delta = (next_point[0] - point[0], next_point[1] - point[1])
            if delta[0] * direction[1] - delta[1] * direction[0] != 0 or delta[0] * direction[0] + delta[1] * direction[1] <= 0:
                raise ContractError('endpoint-frame', ctx.path, ctx.world((int(point[0]), a[1], int(point[1]))), 'First and last tangents must point out of the host')
        if curve.length > self.max_length or curve.length < 7:
            raise ContractError('connection-domain', ctx.path, conditions='Route length exceeds the negotiated preparation budget')
        center = curve.cells()
        if len(set(center)) != len(center):
            raise ContractError('connection-domain', ctx.path, conditions='Need a simple route of length 7..maximum')
        radius = self.width // 2 + 1
        deck = curve.ribbon(radius + .2)
        station = {p: min(range(len(center)), key=lambda i: (math.dist(p, center[i]), i)) for p in deck}
        count = len(center)
        # Preserve the host's offered mouth width, then flare to the body width.
        mouth_widths = []
        for port in (pa, pb):
            horizontal = [port.frame.inverse(q)[0] for q in port.region.corners()]
            mouth_widths.append(max(horizontal) - min(horizontal) + 1)
        if min(mouth_widths) < 3:
            raise ContractError('endpoint-width', ctx.path, ctx.world(a), 'Connection needs an offered mouth at least three cells wide')
        deck = tuple(p for p in deck if (station[p] >= 3 or abs(pa.frame.inverse(ctx.world((p[0], a[1], p[1])))[0]) <= mouth_widths[0] // 2) and (station[p] < count - 3 or abs(pb.frame.inverse(ctx.world((p[0], b[1], p[1])))[0]) <= mouth_widths[1] // 2))
        heights = [lattice((a[1] - 1) * (1 - i / (count - 1)) + (b[1] - 1) * i / (count - 1) + self.rise * math.sin(math.pi * i / (count - 1))) for i in range(count)]
        beds = {}
        waters = {}
        endpoint_regions = (pa.region, pb.region)
        for x, z in deck:
            cap = max(a[1], b[1]) + self.rise + 2
            bed = ctx.elevation(x, z, maximum=cap)
            water = ctx.water(x, z)
            beds[(x, z)] = bed
            waters[(x, z)] = water
            i = station[(x, z)]
            heights[i] = max(heights[i], bed, water + 1 if water is not None else bed)
        # Minimal upward propagation, preserving fixed endpoint elevations.
        for i in range(1, count):
            heights[i] = max(heights[i], heights[i - 1] - 1)
        for i in range(count - 2, -1, -1):
            heights[i] = max(heights[i], heights[i + 1] - 1)
        if heights[0] != a[1] - 1 or heights[-1] != b[1] - 1:
            raise ContractError('endpoint-grade', ctx.path, ctx.world(a), 'Measured ground/water cannot meet the locked endpoint heights')
        outer = set(perimeter(deck))
        guards = []
        clear = []
        all_deck = []
        writes = []
        for x, z in deck:
            i = station[(x, z)]
            y = heights[i]
            q = ctx.world((x, y, z))
            host_region = any(region.contains(q) for region in endpoint_regions)
            is_guard = (x, z) in outer and 2 < i < count - 3 and not host_region
            all_deck.append((x, y, z))
            if is_guard:
                guards.append((x, y + 1, z))
            else:
                clear.extend(((x, y + 1, z), (x, y + 2, z)))
            existing = ctx.view.state(q)
            if solid(existing) and host_region:
                continue
            writes.append((x, y, z))
        anchors = []
        for x, z in sorted(deck, key=lambda p: (station[p], p)):
            if any(abs(x - u) + abs(z - v) <= self.max_span for u, v in anchors):
                continue
            # Prefer the central route where possible, then cover wide diagonal corners.
            near = center[station[(x, z)]]
            anchor = near if near in beds else (x, z)
            if anchor in anchors:
                anchor = (x, z)
            anchors.append(anchor)
        columns = []
        for x, z in anchors:
            top = heights[station[(x, z)]] - 1
            bed = min(beds[(x, z)], top)
            while not ctx.view.supports(ctx.world((x, bed, z))):
                bed -= 1
                if top - bed > self.max_post:
                    raise ContractError('connection-support-height', ctx.path, ctx.world((x, top, z)))
            if not ctx.view.supports(ctx.world((x, bed - 1, z))):
                raise ContractError('connection-bed', ctx.path, ctx.world((x, bed - 1, z)), 'Need two actual supporting bed cells')
            columns.append((x, bed, z, top, waters[(x, z)] is not None))
        fill = sum(max(0, top - bed) for x, bed, z, top, wet in columns)
        if fill > self.fill_limit:
            raise ContractError('connection-fill-limit', ctx.path, conditions=f'{fill}>{self.fill_limit}')
        roof = tuple((x, y + 5, z) for x, y, z in all_deck if self.covered and 3 < station[(x, z)] < count - 4)
        guard_points = tuple(guards)
        low = min(min(beds.values()) - 1, min(heights))
        envelope = bounds2(deck, low, max(heights) + 7).expand(1)
        center_route = tuple((x, heights[i] + 1, z) for i, (x, z) in enumerate(center))
        rules = (Rule('route', (a, *center_route, b), envelope, phase='complete'), Rule('support', tuple(all_deck)), Rule('clear', tuple(clear)), Rule('extension.span', tuple(all_deck), data={'anchors': [(x, 0, z) for x, z in anchors], 'maximum': self.max_span}))
        return Contract(envelope, ports=(Port('start', 'access', Frame(a), Box(a, a), 8), Port('end', 'access', Frame(b), Box(b, b), 8)), rules=rules, decisions={'curve': curve.points, 'center': center, 'profile': heights, 'deck': all_deck, 'writes': writes, 'guards': guard_points, 'columns': columns, 'roof': roof, 'fill': fill, 'width': self.width, 'covered': self.covered, 'rise': self.rise, 'endpoint_regions': [[r.lo, r.hi] for r in endpoint_regions]}, reads=(envelope,))

    def realize(self, ctx, contract):
        d = contract.decisions
        terrain = (Binding(self.terrain, 'construction', 'fill', ctx.frame.box(contract.envelope)),)
        p = Plan(children=[Child('supports', Trestles(tuple(d['columns']), self.max_post), bindings=terrain), Child('deck', Surface(tuple(d['writes'])), bindings=terrain), Child('guards', Guards(tuple(d['guards']), self.covered, tuple(d['roof'])))])
        p.relations.extend(((ctx.path, 'connects', self.start.host), (ctx.path, 'connects', self.end.host)))
        return p


def connection(key, start, end, terrain, **parameters):
    """Create a fully bound link through public endpoint references."""
    return Child(key, Link(start, end, terrain, **parameters), bindings=(start.binding(), end.binding()))
