# Architectural declarations and bounded sampling domains.
from __future__ import annotations

from dataclasses import dataclass, field, fields, is_dataclass
from typing import Any


@dataclass(frozen=True)
class Choice:
    """Declare a finite domain of explicit alternatives.

    Args:
        values: Two or more architectural values, or one constrained value.
    """
    values: tuple

    def __init__(self, *values):
        if not values:
            raise ValueError('Choice needs at least one value')
        object.__setattr__(self, 'values', tuple(values))


@dataclass(frozen=True)
class Span:
    """Declare an inclusive integer domain.

    Args:
        low: Minimum value.
        high: Maximum value.
        step: Positive increment from low.
    """
    low: int
    high: int
    step: int = 1

    def __post_init__(self):
        if any(type(v) is not int for v in (self.low, self.high, self.step)) or self.low > self.high or self.step < 1:
            raise ValueError('Span requires integer low <= high and step >= 1')
        if len(range(self.low, self.high + 1, self.step)) > 256:
            raise ValueError('Span supports at most 256 declared values; narrow the domain')


@dataclass(frozen=True)
class Materials:
    """Assign coherent material roles independently of the building composition."""
    wall: str = 'stone'
    frame: str = 'oak'
    roof: str = 'pale_stone'
    trim: str = 'spruce'
    floor: str = 'oak'
    accent: str = 'green'
    shutters: bool = True


@dataclass(frozen=True)
class Roof:
    """Describe a backed roof surface; ridge direction is relative to the plan."""
    form: Any = 'gable'
    ridge: Any = 'long'
    pitch: Any = 1.0
    eaves: int = 1
    dormers: Any = 0
    cupola: Any = False


@dataclass(frozen=True)
class Level:
    """Allocate domestic functions, partitioning and floor proportions."""
    rooms: tuple[str, ...] = ('living', 'kitchen', 'sleeping', 'storage')
    layout: Any = 'open'
    height: int = 5
    inset: int = 0
    wall: str | None = None
    attic: bool = False


@dataclass(frozen=True)
class Join:
    """Connect a new volume to an existing volume by a shared wall and doorway."""
    to: str
    side: Any = 'east'
    align: Any = 'center'
    level: int = 0


@dataclass(frozen=True)
class Volume:
    """Compose an enclosed mass with levels, an envelope and vertical circulation."""
    name: str
    width: Any = 11
    depth: Any = 11
    levels: tuple[Level, ...] = (Level(),)
    roof: Roof | None = field(default_factory=Roof)
    join: Join | None = None
    envelope: str = 'framed'
    circulation: Any = 'stairs'
    raised: int = 0
    supports: str = 'foundation'
    root_spread: Any = 4
    root_crown: Any = 5


@dataclass(frozen=True)
class Attachment:
    """Attach usable outdoor space at a selected wall and occupied level."""
    kind: str
    to: str
    side: Any = 'south'
    level: int = 0
    depth: Any = 4
    width: Any = 'full'
    cover: str = 'open'


@dataclass(frozen=True)
class Feature:
    """Reserve a site plot with a path to its working edge."""
    kind: str
    side: Any = 'east'
    scale: Any = 7


@dataclass(frozen=True)
class Site:
    """Define the terrain relationship and planting policy."""
    terrain: str = 'meadow'
    margin: int = 5
    planting: str = 'temperate'
    rise: int = 5
    water_depth: int = 2
    paving: str = 'garden'


@dataclass(frozen=True)
class Entrance:
    """Choose a volume, wall, level and bay for the public approach."""
    volume: str
    side: str = 'south'
    level: int = 0
    bay: Any = 'center'


@dataclass(frozen=True)
class Limits:
    """Bound generation cost independently of architectural dimensions."""
    horizontal: int = 128
    vertical: int = 96
    volume: int = 1_500_000
    blocks: int = 300_000


@dataclass(frozen=True)
class Design:
    """Compose one coherent property without a building-family discriminator."""
    name: str
    volumes: tuple[Volume, ...]
    materials: Materials = field(default_factory=Materials)
    site: Site = field(default_factory=Site)
    entrance: Entrance | None = None
    attachments: tuple[Attachment, ...] = ()
    features: tuple[Feature, ...] = ()
    limits: Limits = field(default_factory=Limits)


class DesignError(ValueError):
    """Report an unsatisfiable or unsupported architectural constraint."""


def domain(value):
    """Expand a single bounded parameter into its declared alternatives."""
    if isinstance(value, Choice):
        return value.values
    if isinstance(value, Span):
        return tuple(range(value.low, value.high + 1, value.step))
    return (value,)


def choose(value, rng, allowed=None, context='parameter'):
    """Sample only feasible declared choices; reject contradictory intent."""
    candidates = [v for v in domain(value) if allowed is None or allowed(v)]
    if not candidates:
        raise DesignError(f'{context}: no feasible value in {domain(value)!r}')
    if hasattr(rng, 'pick'):
        return rng.pick(candidates)
    return candidates[rng.randrange(len(candidates))] if len(candidates) > 1 else candidates[0]


def document(value):
    """Convert a declaration to a stable, inspectable JSON structure."""
    if is_dataclass(value):
        return {'type': type(value).__name__, **{f.name: document(getattr(value, f.name)) for f in fields(value)}}
    if isinstance(value, (tuple, list)):
        return [document(v) for v in value]
    if isinstance(value, dict):
        return {str(k): document(v) for k, v in value.items()}
    return value
