# Resolve a constrained lodge grammar into connected, dimensioned volumes.
from __future__ import annotations

from dataclasses import asdict, dataclass, field
import random

DOMAINS = {
    'topology': ('single', 'ell', 'twin', 'courtyard', 'staggered'),
    'width': (15, 17, 19),
    'depth': (17, 19, 21),
    'floors': (1, 2, 3),
    'ridge': ('z', 'x'),
    'profile': ('chalet', 'steep', 'bell'),
    'wing_size': (9, 11, 13),
    'wing_floors': (1, 2),
    'wing_ridge': ('parallel', 'cross'),
    'placement': ('front', 'middle', 'rear'),
    'tower': ('none', 'stone', 'timber'),
    'tower_extra': (1, 2),
    'tower_width': (9, 11),
    'conservatory': (False, True),
    'porch': ('entry', 'full', 'wrap'),
    'balcony': (False, True),
    'dormers': (0, 1, 2),
    'terrain': ('garden', 'terrace', 'cliff'),
    'approach': ('straight', 'landing'),
    'entrance': ('center', 'left', 'right'),
    'layout': ('open', 'cross', 'suites'),
    'stair_side': ('left', 'right'),
    'palette': ('honey', 'ivory', 'ochre'),
    'trees': (3, 4, 5),
}
LIMITS = {'max_axis': 112, 'max_height': 96, 'max_volume': 900000, 'floor_height': 6, 'tree_height': (15, 24)}
DEFAULT_SEED = 20260924


@dataclass
class Volume:
    """A connected room stack, with a roof in local cross-ridge coordinates."""
    name: str
    x0: int
    z0: int
    x1: int
    z1: int
    base: int
    floors: int
    axis: str = 'z'
    profile: str = 'chalet'
    kind: str = 'house'
    attic: bool = False
    parent: str | None = None
    floor_step: int = 6

    @property
    def levels(self):
        return [self.base + i * self.floor_step for i in range(self.floors + int(self.attic))]

    @property
    def uc(self):
        return (self.x0 + self.x1) // 2 if self.axis == 'z' else (self.z0 + self.z1) // 2

    @property
    def u0(self):
        return self.x0 if self.axis == 'z' else self.z0

    @property
    def u1(self):
        return self.x1 if self.axis == 'z' else self.z1

    @property
    def v0(self):
        return self.z0 if self.axis == 'z' else self.x0

    @property
    def v1(self):
        return self.z1 if self.axis == 'z' else self.x1

    def xyz(self, u, y, v):
        return (u, y, v) if self.axis == 'z' else (v, y, u)

    def uv(self, x, z):
        return (x, z) if self.axis == 'z' else (z, x)

    def contains(self, x, z, margin=0):
        return self.x0 - margin <= x <= self.x1 + margin and self.z0 - margin <= z <= self.z1 + margin

    def roof(self, x, z):
        u, _ = self.uv(x, z)
        d = (self.u1 - self.u0) // 2 - abs(u - self.uc)
        eave = self.levels[-1] + (2 if self.attic else 5)
        if self.kind == 'glass':
            return eave + min(x - self.x0, self.x1 - x, z - self.z0, self.z1 - z, 3)
        if self.kind in ('stone', 'timber'):
            d = min(x - self.x0, self.x1 - x, z - self.z0, self.z1 - z)
            eave += 1
        rise = d if self.profile == 'chalet' or d < 0 else 2 * d if self.profile == 'steep' else d + max(0, d - 2)
        return eave + rise

    def interior(self, x, y, z):
        return self.x0 < x < self.x1 and self.z0 < z < self.z1 and self.base < y < self.roof(x, z)


@dataclass
class Plan:
    seed: int
    parameters: dict
    volumes: list[Volume]
    links: list[dict]
    entrance: tuple
    porch: tuple
    terrace_front: int
    approach: list[dict]
    tree_sites: list[tuple]
    origin: tuple
    size_xyz: tuple
    limits: dict
    overrides: dict = field(default_factory=dict)

    def record(self):
        return asdict(self)

    def signature(self):
        """Describe actual relative massing and roof structure, independent of seed."""
        m = self.volumes[0]
        return tuple((v.x0 - m.x0, v.z0 - m.z0, v.x1 - v.x0, v.z1 - v.z0, tuple(y - m.base for y in v.levels), v.axis, v.profile, v.kind) for v in self.volumes)


def resolve(seed: int, overrides: dict | None = None, limits: dict | None = None) -> Plan:
    """Resolve one seed and optional architectural constraints without retries.

    Args:
        seed (int): Any signed 64-bit integer.
        overrides (dict): Explicit values from DOMAINS.
        limits (dict): Resource limits, constrained to the documented keys.

    Returns:
        Plan: Dimensioned, connected design with computed export bounds.
    """
    if type(seed) is not int or not -(2**63) <= seed < 2**63:
        raise ValueError('seed must be a signed 64-bit integer')
    overrides = dict(overrides or {})
    for key, value in overrides.items():
        if key not in DOMAINS or value not in DOMAINS[key] or type(value) is not type(DOMAINS[key][0]):
            raise ValueError(f'invalid {key}={value!r}; domain: {DOMAINS.get(key, "unknown parameter")}')
    lim = dict(LIMITS)
    if limits:
        if set(limits) - set(LIMITS):
            raise ValueError('unknown resource limit')
        lim.update(limits)
    for key in ('max_axis', 'max_height', 'max_volume'):
        if type(lim[key]) is not int or lim[key] <= 0:
            raise ValueError(f'{key} must be a positive integer')
    heights = lim['tree_height']
    if not isinstance(heights, (tuple, list)) or len(heights) != 2 or any(type(h) is not int for h in heights) or not 10 <= heights[0] <= heights[1] <= 32:
        raise ValueError('tree_height must be [minimum, maximum] within 10..32')
    if lim['floor_height'] != 6:
        raise ValueError('floor_height is fixed at 6 for the stair components')
    rng = random.Random(seed)
    p = {k: rng.choice(v) for k, v in DOMAINS.items()}
    p.update(overrides)

    def condition(key, allowed, reason):
        if p[key] not in allowed:
            if key in overrides:
                raise ValueError(f'{key}={p[key]!r}: {reason}; allowed {allowed}')
            p[key] = rng.choice(tuple(allowed))

    condition('wing_floors', range(1, min(2, p['floors']) + 1), 'wings cannot exceed the main full-storey count')
    if p['floors'] == 1:
        condition('balcony', (False,), 'balconies require a full upper storey')
    if p['topology'] in ('twin', 'courtyard'):
        condition('porch', ('entry', 'full'), 'side wings occupy the wraparound interface')
    base = {'garden': 4, 'terrace': 7, 'cliff': 16}[p['terrain']]
    w, d = p['width'], p['depth']
    volumes = [Volume('main', 0, 0, w - 1, d - 1, base, p['floors'], p['ridge'], p['profile'], attic=True)]
    links = []

    def add(name, host, side, width, length, alignment, floors, axis, kind='house', profile=None):
        if side in ('left', 'right'):
            z0 = alignment
            x0 = host.x0 - width + 1 if side == 'left' else host.x1
            v = Volume(name, x0, z0, x0 + width - 1, z0 + length - 1, base, floors, axis, profile or p['profile'], kind, parent=host.name)
            z = (max(host.z0, v.z0) + min(host.z1, v.z1)) // 2
            x = host.x0 if side == 'left' else host.x1
            links.append({'a': host.name, 'b': name, 'x': x, 'z': z, 'axis': 'x', 'floor': base})
        else:
            x0 = alignment
            v = Volume(name, x0, host.z1, x0 + width - 1, host.z1 + length - 1, base, floors, axis, profile or p['profile'], kind, parent=host.name)
            x = (max(host.x0, v.x0) + min(host.x1, v.x1)) // 2
            links.append({'a': host.name, 'b': name, 'x': x, 'z': host.z1, 'axis': 'z', 'floor': base})
        volumes.append(v)
        return v

    main = volumes[0]
    wing_axis = p['ridge'] if p['wing_ridge'] == 'parallel' else ('x' if p['ridge'] == 'z' else 'z')
    count = {'single': 0, 'ell': 1, 'twin': 2, 'courtyard': 2, 'staggered': 1}[p['topology']]
    first_side = rng.choice(('left', 'right'))
    if p['porch'] == 'wrap' and p['conservatory'] and count == 1:
        first_side = 'right'
    sides = [first_side] if count == 1 else ['left', 'right'] if count == 2 else []
    for i, side in enumerate(sides):
        width = p['wing_size']
        length = rng.choice((11, 13, 15))
        z0 = {'front': -2, 'middle': (d - length) // 2, 'rear': d - length + 2}[p['placement']]
        if p['topology'] == 'courtyard':
            z0, length = -rng.choice((5, 7)), d - 3
        elif i:
            z0 += rng.choice((-2, 2))
        add('wing_' + side, main, side, width, length, z0, p['wing_floors'], wing_axis)
    rear_host = main
    if p['topology'] == 'staggered':
        rear_host = add('rear_wing', main, 'rear', p['wing_size'], rng.choice((9, 11, 13)), (w - p['wing_size']) // 2, p['wing_floors'], 'z')
    if p['tower'] != 'none':
        tw = p['tower_width']
        add('tower', rear_host, 'rear', tw, tw, (rear_host.x0 + rear_host.x1 - tw + 1) // 2, min(5, p['floors'] + p['tower_extra']), 'z', p['tower'], 'bell' if p['tower'] == 'timber' else 'chalet')
    if p['conservatory']:
        host = next((v for v in volumes if v.name == 'wing_right'), main)
        length = min(11, host.z1 - host.z0 + 1)
        add('conservatory', host, 'right', rng.choice((7, 9)), length, (host.z0 + host.z1 - length + 1) // 2, 1, 'z', 'glass', 'chalet')
    free_sides = [side for side in ('left', 'right') if not any(v.name == 'wing_' + side for v in volumes) and not (side == 'right' and p['conservatory'])]
    if not free_sides:
        condition('porch', ('entry', 'full'), 'no free side for a wraparound gallery')
    p['wrap_side'] = free_sides[0] if free_sides and p['porch'] == 'wrap' else None
    ex = w // 2 + {'center': 0, 'left': -3, 'right': 3}[p['entrance']]
    porch_width = 7 if p['porch'] == 'entry' else w
    porch_x0 = max(0, min(ex - porch_width // 2, w - porch_width))
    porch = (porch_x0, -4, porch_x0 + porch_width - 1, -1)
    front = min(v.z0 for v in volumes) - 6
    approach = []
    rises = base - 1
    landing_length = 3 if p['approach'] == 'landing' else 0
    startz = front - rises - landing_length - 1
    y, z = 1, startz
    approach.append({'x': ex, 'z': z, 'y': y, 'kind': 'landing'})
    for i in range(rises):
        if i == rises // 2 and landing_length:
            for _ in range(landing_length):
                z += 1
                approach.append({'x': ex, 'z': z, 'y': y, 'kind': 'landing'})
        y, z = y + 1, z + 1
        approach.append({'x': ex, 'z': z, 'y': y, 'kind': 'step'})
    for zz in range(z + 1, 1):
        approach.append({'x': ex, 'z': zz, 'y': base, 'kind': 'landing'})
    # Dormers attach to the main attic, far enough inside the wall to open into it.
    for i in range(p['dormers']):
        side = -1 if i == 0 else 1
        vc = main.v0 + 5 if i == 0 else main.v1 - 5
        ua, ub = (main.u0 - 1, main.uc) if side < 0 else (main.uc, main.u1 + 1)
        a, _, b = main.xyz(ua, 0, vc - 2)
        c, _, e = main.xyz(ub, 0, vc + 2)
        volumes.append(Volume(f'dormer_{i}', a, b, c, e, main.levels[-1], 1, 'x' if main.axis == 'z' else 'z', 'chalet', 'dormer', parent='main'))
    xmin = min(v.x0 for v in volumes) - 7
    xmax = max(v.x1 for v in volumes) + 7
    zmin = startz - 3
    zmax = max(v.z1 for v in volumes) + 7
    sites = [(xmin + 2, max(1, main.z1 - 2)), (xmax - 2, main.z1 + 2), ((xmin + xmax) // 2 - 6, zmax - 2), (xmin + 3, main.z0 - 1), (xmax - 2, main.z0 + 2)]
    trees = [(x, z, rng.randint(*lim['tree_height'])) for x, z in sites[:p['trees']]]
    maxroof = max(v.roof((v.x0 + v.x1) // 2, (v.z0 + v.z1) // 2) for v in volumes)
    height = max(maxroof + 7, base + lim['tree_height'][1] + 3)
    origin = (2 - xmin, 0, 2 - zmin)
    size = (xmax - xmin + 5, height + 1, zmax - zmin + 5)
    if max(size[0], size[2]) > lim['max_axis'] or size[1] > lim['max_height'] or size[0] * size[1] * size[2] > lim['max_volume']:
        raise ValueError(f'design bounds {size} exceed resource limits {lim}')
    return Plan(seed, p, volumes, links, (ex, base + 1, 0), porch, front, approach, trees, origin, size, lim, overrides)
