# Frozen extension inputs, actual principal/contact signatures and retained outcomes.
import hashlib
import json
import time
from pathlib import Path
from dataclasses import replace
from collections import Counter
from hearth import Scene, Box, Frame, Binding
from hearth.kernel import ContractError, SearchExhausted, validate
from hearth.environment import Terrain, gradient, wave, radial
from hearth.randomness import Scope
from hearth.persistence import semantic_digest, export_scene
from hearth.blocks import solid
from .geometry import Polyline, Polygon
from .sites import Platform
from .interfaces import Endpoint
from .circulation import Link
from .boundaries import BoundaryChain, IlluminateBoundary
from .halls import Pavilion
from .programs import frozen_site, branching_campus, perimeter_cluster

PROBE_DOMAIN = Box((-8, -4, -8), (45, 52, 45))


def probe_expression(environment_seed):
    rng = Scope(environment_seed).stream('probe-field')
    return (gradient(rng.choice((-.025, 0, .035)), rng.choice((0, .015)), 2) + wave(rng.choice((0, .35, .7)), .11, .09, rng.random() * 6) + radial(16, 18, 14, -rng.choice((0, 1, 2)))).clamp(-1, 5), rng.choice((None, 3))


def freeze_probe(environment_seed):
    field, water = probe_expression(environment_seed)
    s = Scene(environment_seed, domain=PROBE_DOMAIN)
    s.place('land', Terrain(s.domain, field, water))
    return s.finalize()


def connector_scene(environment, structure_seed):
    s = environment.fork(seed=structure_seed)
    rng = Scope(structure_seed).stream('connection-intent')
    s.place('a', Platform('/land'))
    s.place('b', Platform('/land'), frame=Frame((18, 0, 27)))
    a, b = Endpoint('/a', 'south'), Endpoint('/b', 'north')
    link = Link(a, b, '/land', via=((rng.choice((9, 12, 15)), rng.choice((16, 18, 20))),), width=rng.choice((3, 5)), rise=rng.choice((0, 1, 2)), covered=rng.choice((True, False)))
    s.place('link', link, bindings=(a.binding(), b.binding()))
    return s.finalize()


def boundary_scene(environment, structure_seed):
    rng = Scope(structure_seed).stream('boundary-intent')
    controls = ((0, 0), (8, rng.choice((0, 10, 23))), (28, rng.choice((0, 5, 18))), (37, 18))
    curve = Polyline(((0, 0), (37, 18))) if structure_seed == 0 else Polyline.bezier(controls)
    s = environment.fork(seed=structure_seed)
    s.place('boundary', BoundaryChain(curve, height=rng.choice((1, 2, 3)), spacing=rng.choice((4, 7, 10))))
    s.place('lights', IlluminateBoundary('/boundary'))
    return s.finalize()


def hall_scene(seed, environment_seed=None):
    rng = Scope(seed).stream('hall-intent')
    domain = Box((-12, -4, -12), (42, 65, 42))
    es = seed if environment_seed is None else environment_seed
    field = gradient(.012, -.008, 4) + wave(.5, .1, .12, Scope(es).stream('hall-field').random() * 6)
    env = Scene(es, domain=domain)
    env.place('land', Terrain(domain, field))
    env.finalize()
    s = env.fork(seed=seed)
    shape = Polygon.chamfered(rng.choice((13, 17, 21, 25)), rng.choice((13, 17, 21)), rng.choice((0, 2, 4)))
    s.place('hall', Pavilion(shape, '/land', rng.choice(('library', 'workshop', 'living')), ('north', 'south', 'east') if rng.choice((True, False)) else ('north', 'west'), height=rng.choice((5, 6, 7)), roof_run=rng.choice((2, 3)), lantern=rng.choice((True, False))))
    return s.finalize()


def signatures(scene):
    """Measure current semantic geometry, omitting cosmetic material and contact height."""
    halls = []
    links = []
    boundaries = []
    organization = []
    for path, node in sorted(scene.nodes.items()):
        d = node.contract.decisions
        if node.type == 'extension.pavilion':
            footprint = sorted((node.frame.inverse(p)[0], node.frame.inverse(p)[2]) for p in scene.cells(path + '/floor'))
            halls.append({'footprint': footprint, 'height': d['height'], 'function': d['purpose'], 'entrances': d['entrances'], 'roof_run': d['roof_run'], 'lantern': d['lantern']})
        if node.type in ('extension.bridge', 'extension.gallery'):
            pts = [node.frame.inverse(p) for p in scene.cells(path + '/deck')]
            x0, z0 = d['center'][0]
            links.append({'body': sorted((p[0] - x0, p[2] - z0) for p in pts), 'width': d['width'], 'covered': d['covered'], 'rise': d['rise']})
        if node.type == 'extension.boundary-chain':
            pts = [node.frame.inverse(p) for p in scene.cells(path, True) if 'boundary-lantern' not in scene.nodes[scene.inspect(p)['instance']].type]
            x0 = min(p[0] for p in pts)
            z0 = min(p[2] for p in pts)
            columns = {}
            for x, y, z in pts:
                columns.setdefault((x, z), []).append(y)
            boundaries.append({'plan': sorted(set((p[0] - x0, p[2] - z0) for p in pts)), 'section_heights': sorted({max(ys) - min(ys) + 1 for ys in columns.values()})})
        if node.type == 'extension.campus':
            vertex_keys = d['vertices']
            vertices = []
            for key in vertex_keys:
                v = scene.nodes[path + '/' + key]
                at = node.frame.inverse(v.frame.origin)
                vertices.append((at[0], at[2], v.capability.tags))
            topology = sorted((vertex_keys.index(a), pa, vertex_keys.index(b), pb, covered, width) for a, pa, b, pb, covered, width in d['edges'])
            organization.append({'positions': vertices, 'topology': topology})
    from hearth.assessment import principal_signature
    legacy = principal_signature(scene)[1]['buildings']
    principal = {'halls': halls, 'links': links, 'boundaries': boundaries, 'organization': organization, 'legacy': legacy}
    contact = []
    fill = 0
    replaced = 0
    excavated = 0
    edit_occupied = 0
    for path, node in sorted(scene.nodes.items()):
        if node.type in ('extension.trestles', 'adapter.foundation', 'adapter.approach'):
            contact.append((node.type, node.contract.decisions))
            fill += len(scene.cells(path))
        if node.type == 'extension.curved-boundary':
            contact.append((node.type, [node.frame.inverse(p) for r in node.contract.rules if r.kind == 'support' for p in r.cells]))
        if node.capability and set(node.capability.tags).intersection(('siteworks', 'path')):
            edit_occupied += len(scene.cells(path))
            replaced += len(scene.displaced.get(path, {}))
            excavated += sum(solid(old.state) and (p not in scene.blocks or not solid(scene.blocks[p].state)) for p, old in scene.displaced.get(path, {}).items())
    digest = lambda x: hashlib.sha256(json.dumps(x, sort_keys=True, default=list).encode()).hexdigest()
    interiors = [n for n in scene.nodes.values() if n.type == 'extension.hall-interior']
    return {'principal_signature': digest(principal), 'structure': principal, 'contact_signature': digest(contact), 'contact_cells': fill, 'contact_replacements': replaced, 'sitework_occupied_cells': edit_occupied, 'excavated_solid_cells': excavated, 'furnishing_bays': sum(n.contract.decisions['placed_bays'] for n in interiors), 'omitted_optional_furnishing_bays': sum(n.contract.decisions['omitted_optional'] for n in interiors)}


def measured(label, recipe, inputs, artifact=None):
    start = time.perf_counter()
    row = {'label': label, 'inputs': inputs, 'candidate_attempts': 1, 'accepted_candidates': 0, 'retries': 0}
    try:
        scene = recipe()
        scene.finalize()
        row.update(status='validated', accepted_candidates=1, blocks=len(scene.blocks), instances=len(scene.nodes), digest=semantic_digest(scene), **signatures(scene))
        if artifact:
            export_scene(scene, artifact)
            row['artifact'] = str(artifact)
    except ContractError as exc:
        limits = ('site-relief', 'support-height', 'connection-support-height', 'connection-fill-limit', 'boundary-grade', 'endpoint-grade', 'approach-detour', 'connection-domain')
        row.update(status='budget-exhausted' if isinstance(exc, SearchExhausted) else ('incompatible' if exc.diagnostic.rule in limits else 'defect'), diagnostic=exc.diagnostic.__dict__, all_diagnostics=[d.__dict__ for d in getattr(exc, 'diagnostics', [exc.diagnostic])])
    except Exception as exc:
        row.update(status='defect', diagnostic={'exception': type(exc).__name__, 'message': str(exc)})
    row['seconds'] = round(time.perf_counter() - start, 4)
    return row


def run(output, root_seed=0, artifacts=False, sample_output=Path('samples/extensions')):
    output = Path(output)
    output.mkdir(parents=True, exist_ok=True)
    rows = []
    frozen = {}
    inputs = []
    for es in range(root_seed * 100, root_seed * 100 + 8):
        frozen[es] = freeze_probe(es)
        field, water = probe_expression(es)
        inputs.append({'environment_seed': es, 'expression': field.expression(), 'water': water, 'digest': semantic_digest(frozen[es])})
    (output / 'frozen-inputs.json').write_text(json.dumps(inputs, indent=2))

    def record(label, fn, parameters, keep=False):
        path = Path(sample_output) / (label + '.litematic') if keep and artifacts else None
        row = measured(label, fn, parameters, path)
        rows.append(row)
        print(label, row['status'], flush=True)

    for i, es in enumerate(frozen):
        for bs in range(6):
            record(f'connector-{i}-{bs}', lambda e=frozen[es], b=bs: connector_scene(e, b), {'environment': inputs[i], 'structure_seed': bs}, (i, bs) in ((0, 0), (1, 2), (4, 3), (7, 5)))
        assert semantic_digest(frozen[es]) == inputs[i]['digest']
    for es in range(4):
        # Independent dry substrates use the same field expression with water omitted.
        field, _ = probe_expression(es + root_seed * 100)
        env = Scene(es + root_seed * 100, domain=PROBE_DOMAIN)
        env.place('land', Terrain(env.domain, field))
        env.finalize()
        for bs in range(4):
            record(f'boundary-{es}-{bs}', lambda e=env, b=bs: boundary_scene(e, b), {'environment_seed': es + root_seed * 100, 'expression': field.expression(), 'structure_seed': bs}, (es, bs) in ((0, 1), (3, 3)))
    for seed in range(12):
        record(f'hall-{seed}', lambda b=seed: hall_scene(b, root_seed * 100 + b), {'structure_seed': seed, 'environment_seed': root_seed * 100 + seed}, seed in (0, 4, 7))
    for name, program in (('campus', branching_campus), ('perimeter', perimeter_cluster)):
        for seed in range(6):
            environment = frozen_site(root_seed * 100 + seed)
            identity = semantic_digest(environment)

            def recipe(e=environment, b=seed, p=program):
                s = e.fork(seed=b)
                s.place('campus', p(b))
                return s.finalize()

            record(f'{name}-{seed}', recipe, {'program': name, 'structure_seed': seed, 'environment_seed': root_seed * 100 + seed, 'frozen_digest': identity}, seed in (0, 3))
            assert semantic_digest(environment) == identity
    for i in range(24):
        es = 2000 + root_seed * 100 + i
        rng = Scope(es).stream('stress')
        field = (gradient(rng.choice((0, .12, .28)), rng.choice((0, -.12, .15)), 2) + wave(rng.choice((0, 2, 4)), .13, .1, rng.random() * 6)).clamp(-2, 18)
        water = rng.choice((None, 3, 9, 12))
        env = Scene(es, domain=PROBE_DOMAIN)
        env.place('land', Terrain(env.domain, field, water))
        env.finalize()
        record(f'stress-{i:02}', lambda e=env, b=i % 6: connector_scene(e, b), {'environment_seed': es, 'structure_seed': i % 6, 'expression': field.expression(), 'water': water})
    (output / 'results.json').write_text(json.dumps(rows, indent=2))
    summary = {}
    for name in ('connector', 'boundary', 'hall', 'campus', 'perimeter', 'stress'):
        part = [r for r in rows if r['label'].startswith(name + '-')]
        good = [r for r in part if r['status'] == 'validated']
        summary[name] = {'inputs': len(part), 'outcomes': dict(Counter(r['status'] for r in part)), 'candidate_attempts': sum(r['candidate_attempts'] for r in part), 'accepted_candidates': sum(r['accepted_candidates'] for r in part), 'retries': sum(r['retries'] for r in part), 'principal_signatures': len({r['principal_signature'] for r in good}), 'contact_signatures': len({r['contact_signature'] for r in good}), 'contact_cells_range': [min((r['contact_cells'] for r in good), default=0), max((r['contact_cells'] for r in good), default=0)], 'contact_replacement_range': [min((r['contact_replacements'] for r in good), default=0), max((r['contact_replacements'] for r in good), default=0)], 'seconds': round(sum(r['seconds'] for r in part), 3)}
    (output / 'summary.json').write_text(json.dumps(summary, indent=2))
    if artifacts:
        (Path(sample_output) / 'replay-manifest.json').write_text(json.dumps([{k: r[k] for k in ('label', 'inputs', 'artifact', 'digest', 'status')} for r in rows if 'artifact' in r], indent=2))
    return summary
