#!/usr/bin/env python3
"""Eight editable relationship diagrams. Standard library; no raster generation."""
from pathlib import Path
from html import escape
import json,textwrap,hashlib
HERE=Path(__file__).resolve().parent
ROOT=HERE.parent
OUT=ROOT/'images/diagrams'
BG='#101c26'; PANEL='#1b2c39'; LINE='#66818e'; TEXT='#f4eee2'; MUTED='#becbd0'; COPPER='#efb07b'; TEAL='#8cdbcc'

class Canvas:
    def __init__(self,id,title,subtitle,h=990):
        self.id=id; self.h=h; self.parts=[f'<svg xmlns="http://www.w3.org/2000/svg" width="1320" height="{h}" viewBox="0 0 1320 {h}" role="img" aria-labelledby="title desc"><title id="title">{escape(title)}</title><desc id="desc">{escape(subtitle)}</desc><defs><marker id="arrow" markerWidth="10" markerHeight="10" refX="8" refY="4" orient="auto"><path d="M0,0 L8,4 L0,8" fill="{TEAL}"/></marker></defs><rect width="1320" height="{h}" fill="{BG}"/>']
        self.text(52,48,id,17,COPPER,weight='700')
        self.text(52,90,title,32,weight='700')
        self.lines(52,128,subtitle,112,20,MUTED)
    def text(self,x,y,s,size=21,color=TEXT,weight='400',anchor='start'):
        self.parts.append(f'<text x="{x}" y="{y}" font-family="Arial, sans-serif" font-size="{size}" font-weight="{weight}" fill="{color}" text-anchor="{anchor}">{escape(s)}</text>')
    def lines(self,x,y,s,chars=34,size=20,color=MUTED):
        rows=[]
        for para in s.split('\n'):rows.extend(textwrap.wrap(para,width=chars,break_long_words=False,break_on_hyphens=False)or[''])
        for i,row in enumerate(rows):self.text(x,y+i*(size+8),row,size,color)
        return len(rows)*(size+8)
    def card(self,x,y,w,h,label,body='',tag='',accent=TEAL):
        self.parts.append(f'<rect x="{x}" y="{y}" width="{w}" height="{h}" rx="9" fill="{PANEL}" stroke="{LINE}" stroke-width="1.5"/><rect x="{x}" y="{y}" width="5" height="{h}" rx="2" fill="{accent}"/>')
        # Fit semantic text inside its actual card; keep body >=16px.
        fitted=None
        for title_size in [23,22,21,20]:
          label_rows=textwrap.wrap(label,width=max(16,int((w-44)/(title_size*.52))),break_long_words=False)
          label_y=49 if tag else 31
          for body_size in [19,18,17,16]:
            body_rows=textwrap.wrap(body,width=max(18,int((w-44)/(body_size*.52))),break_long_words=False) if body else []
            body_y=label_y+(len(label_rows)-1)*(title_size+6)+title_size+9
            bottom=body_y+max(0,len(body_rows)-1)*(body_size+6)+5 if body_rows else label_y+(len(label_rows)-1)*(title_size+6)+5
            if bottom<=h-8:fitted=(title_size,label_rows,label_y,body_size,body_rows,body_y);break
          if fitted:break
        if not fitted:raise ValueError(f'Card text exceeds height: {self.id} {label} h={h}')
        ts,lr,ly,bs,br,by=fitted
        if tag:self.text(x+22,y+23,tag,15,accent,weight='700')
        for i,row in enumerate(lr):self.text(x+22,y+ly+i*(ts+6),row,ts,TEXT)
        for i,row in enumerate(br):self.text(x+22,y+by+i*(bs+6),row,bs,MUTED)
    def arrow(self,pts,label='',lx=None,ly=None,dashed=False):
        coords=' '.join(f'{x},{y}'for x,y in pts)
        self.parts.append(f'<polyline points="{coords}" fill="none" stroke="{TEAL}" stroke-width="2.3" stroke-linejoin="round" marker-end="url(#arrow)"'+(' stroke-dasharray="7 6"'if dashed else '')+'/>')
        if label:self.text(lx if lx is not None else pts[0][0]+12,ly if ly is not None else pts[0][1]-12,label,17,TEAL)
    def note(self,y,title,body):
        self.parts.append(f'<rect x="52" y="{y}" width="1216" height="100" rx="7" fill="#223541"/>');self.text(74,y+30,title,20,COPPER,weight='700');self.lines(74,y+60,body,116,18,MUTED)
    def save(self,name):
        self.text(52,self.h-30,'Documentation relationships • Proposed methods • Numerical tuning and physical fit unverified',16,MUTED)
        self.parts.append('</svg>');p=OUT/name;p.write_text('\n'.join(self.parts)+'\n',encoding='utf-8');return p

def main():
    OUT.mkdir(exist_ok=True);figs=[]
    def record(c,name,title,section,source,caption,alt,package='P04',limits=None):
        p=c.save(name);figs.append(dict(id=c.id,title=title,file='images/diagrams/'+name,caption=caption,alt=alt,section_ids=section,source_ids=source,package=package,status='Editable explanatory diagram; documented recommendation, not implemented behavior.',not_established=limits or ['Final dimensions','Numeric recipes or capacities','Runtime behavior'],sha256_file=hashlib.sha256(p.read_bytes()).hexdigest()))

    c=Canvas('C07-D01','Choose scale after choosing the job','Existing capability bands describe available kinds of work. A chosen goal determines the actual functions required.',1030)
    c.card(400,190,520,145,'Useful ordinary workshop','Repair, prepare stock and make an improvement before committing to a larger programme.','EXISTING FOUNDATION')
    c.arrow([(660,335),(660,390)])
    c.card(400,400,520,125,'Prepared field equipment','Real source → typed branch → compatible receiver.','FIRST ELECTRICAL JOB')
    for x in [52,468,884]:c.arrow([(660,525),(660,564),(x+192,564),(x+192,610)])
    c.card(52,610,384,215,'Compact flight branch','Use the credited WP05 path and typed launch services. Return to useful earlier benches.','CHOOSE FLIGHT')
    c.card(468,610,384,215,'Regional repeat work','Add only useful preparation, real buffers and handling for a recurring order.','CHOOSE PRODUCTION SCALE')
    c.card(884,610,384,215,'Frontier service point','Carry first capability. Add local processing after an actual discovery justifies it.','CHOOSE EXPEDITION REACH')
    c.note(866,'Earlier routes remain useful','The branches can coexist or remain small. No requirement to install all 66 responsibilities or every power source.')
    record(c,'c07-d01-workshop-choices.svg','Workshop choices: goals determine the required functions',['C07-WORK-02','C07-WORK-04'],['IP00','IP01','IP02','IND-M39'],'The compact installation supports useful ordinary work and a prepared field receiver. Flight, repeated regional production and frontier service are chosen goals with different requirements, not three compulsory upgrades in sequence.','A common useful workshop and field-equipment job branch into compact flight, regional production and frontier service, with earlier routes retained.','P06')

    c=Canvas('C07-D02','Three working configurations','Read each column from actual incoming stock to a useful result. These are responsibility groupings, not measured floor plans.',1050)
    for x,title,tag,b1,b2,b3 in [
      (52,'Compact','FEWER INSTALLATIONS','Prepared ordinary stock; compatible field cell; actual supply.','Benches and chosen forming work; source, branch and charger.','Repaired equipment and a prepared outing. WP05 flight is a chosen branch.'),
      (468,'Regional','REPEATABLE ORDERS','Accepted raw or prepared feed; real water/power if method needs them.','Selected preparation and process stages; buffers, receivers and order custody.','Known construction parts with actual byproducts and fewer repeated handling steps.'),
      (884,'Frontier','USEFUL LOCAL SERVICE','Imported first equipment, refuge and reserves; protected returned sample.','Vehicle service and sample handling; local process only if equipped for it.','An operable rover, preserved evidence and a supported later Mars application.')]:
        c.text(x+15,208,title,26,COPPER,weight='700');c.text(x+15,238,tag,16,MUTED)
        c.card(x,265,384,150,'Actual inputs',b1);c.arrow([(x+192,415),(x+192,454)])
        c.card(x,464,384,180,'Responsible work',b2);c.arrow([(x+192,644),(x+192,684)])
        c.card(x,694,384,190,'Useful result',b3)
    c.note(909,'Separate services; accessible work','Fuel, breathing stock, process fluid and electricity keep distinct connections. Shared control does not merge duties.')
    record(c,'c07-d02-workshop-configurations.svg','Compact, regional and frontier configurations',['C07-WORK-06','C07-MACH-04'],['IND-M32','IND-M38','IND-M39','IND-M51','IND-M58'],'Three illustrative responsibility groupings compare incoming stock, actual work and useful results. The compact arrangement can remain useful; regional scale pays for its added process; frontier capability arrives before local material upgrades.','Three vertical configurations each contain actual input, responsible work and a useful output; their typed services and accessible work remain separate.','P06')

    c=Canvas('C07-D03','One order, one accountable result','The receiver, reserved stock, partly completed work and returned object retain identity through interruptions.',1050)
    c.card(62,195,495,140,'Inspect and reserve','Identify the same object, compatible input and real available stock.','1 · BEFORE WORK')
    c.card(763,195,495,140,'Cancel before start','Release reservations and return unconsumed material; create no copy.','SUPPORTED EXIT')
    c.arrow([(557,265),(758,265)],'cancel',615,250)
    c.arrow([(310,335),(310,382)])
    c.card(62,392,495,160,'Run the selected operation','Track delivered charge, consumed input and in-process material.','2 · ACTUAL TRANSFORMATION')
    c.card(763,392,495,160,'Pause or park','Retain real progress and custody. No offline production, refill or supply debt.','SUPPORTED INTERRUPTION')
    c.arrow([(557,450),(755,450)],'interrupt',604,436);c.arrow([(763,510),(565,510)],'resume if ready',597,535)
    c.arrow([(310,552),(310,600)])
    c.card(62,610,495,150,'Complete into a real receiver','Return the same repaired object or place the defined product into its receiver.','3 · FINISHED WORK')
    c.card(763,610,495,150,'Output held or service required','Wait or isolate the affected process; do not discard value or duplicate output.','DECLARED REASON')
    c.arrow([(557,678),(755,678)],'blocked output',590,662)
    c.note(815,'Example: field charge','0 < eta ≤ 1; input ≥ Q / eta + declared duty. Q fits the real remaining receiver capacity. Values remain unselected.')
    c.text(65,963,'For parts: retained specimen + committed production + reserve are separate real quantities.',19,MUTED)
    record(c,'c07-d03-order-custody.svg','A bounded workshop order: custody and interruption',['C07-ORDER-01','C07-ORDER-02','C07-ORDER-03'],['IND-M39','IND-M51','IND-M55','IND-M58'],'An order reserves actual stock, performs a selected operation and returns one accountable result. Cancellation, interruption and output blockage have different material consequences. The charging formula is symbolic accounting, not a finalized recipe.','Reserve, run and complete states connect to cancellation, pause/park, and output-held states, retaining actual material and object identity.','P06')

    c=Canvas('C07-D04','Protect real stock before departure','A mission plan names where every allocation physically resides. Each quantity belongs to one location at a time.',1050)
    c.card(375,185,570,140,'Actual prepared mission stock','Separate fuel, breathing supply, water, energy and equipment. A plan is not additional inventory.','PREPARATION')
    c.arrow([(660,325),(660,365),(245,365),(245,400)])
    c.arrow([(660,325),(660,390)]);c.arrow([(660,325),(660,365),(1076,365),(1076,400)])
    c.card(52,410,384,178,'Planetary return reserve','Protected transport/return allocation remains available. No rover repair silently consumes it.','PROTECTED')
    c.card(468,410,384,178,'Refuge stock','Actually stored in a commissioned support place; a cave alone is not breathable.','LOCAL SUPPORT')
    c.card(884,410,384,178,'Outing allocation','A known supported journey has its own actual vehicle, person and instrument needs.','FIELD WORK')
    for x in [52,468,884]:c.arrow([(1076,588),(1076,635),(x+192,635),(x+192,676)])
    c.card(52,686,384,168,'Vehicle / carried stock','Distinguish vehicle storage from what the player can carry away.','NAMED LOCATION')
    c.card(468,686,384,168,'Field camp stock','A cache or commissioned camp holds its actual deposited stock.','NAMED LOCATION')
    c.card(884,686,384,168,'Instrument allocation','Power and sample receivers are assigned to the real instrument duty.','NAMED LOCATION')
    c.note(894,'No double counting','Transfers move the same stock. Moving a cache does not leave a second reserve. An exhausted cell cannot fund another duty.')
    record(c,'c07-d04-mars-supply-custody.svg','Mars preparation: protected return and actual field allocations',['C07-MARS-06','C07-MARS-02'],['DST02','SUP01','PM03','PM04'],'The arrival plan distinguishes protected planetary return, refuge support and outing allocations. Field stock is associated with actual vehicle, personal, camp and instrument locations. No diagrammed box grants capacity or an additional copy of a supply.','Actual prepared mission stock is allocated to protected return, refuge and the outing; the outing is then tracked in vehicle/personal, camp and instrument locations.')

    c=Canvas('C07-D05','Three Mars outings through one readable region','The existing five regions support different depths of commitment. Vehicle access ends where the supported route ends.',1070)
    c.card(55,195,350,140,'Windcut Tablelands','Terraces, forecasts and a supported arrival/return place.','MA-S1')
    c.card(485,195,350,140,'Buried Delta','Compare dry channels and choose the rover stopping point.','MA-S2')
    c.card(915,195,350,140,'Ochre Galleries','Read bedding; use a secureable field camp and actual services.','MA-L1')
    c.arrow([(405,265),(480,265)]);c.arrow([(835,265),(910,265)])
    c.card(485,440,350,165,'Brine Scarps','Protected first sample with imported equipment. Isolated underground pockets.','MA-L2')
    c.card(915,440,350,165,'Rootstone Chambers','Compare separated mineral exposures from a supported instrument platform.','MA-L3')
    c.arrow([(1090,335),(1090,383),(660,383),(660,431)]);c.arrow([(835,520),(910,520)])
    c.card(55,440,350,165,'Known return point','Bring people, actual samples and evidence home. Later visits use the same region.','EVERY OUTING')
    c.arrow([(660,605),(660,640),(230,640),(230,614)])
    c.arrow([(1090,605),(1090,666),(230,666),(230,614)],dashed=True)
    c.card(55,717,350,185,'MA-EXP-01','Surface → gallery shelter → return. Weather/map and a first useful sample.','FIRST SURVEY')
    c.card(485,717,350,185,'MA-EXP-02','Candidate route → brine sample → equipped workshop. Finite catalyst example.','PROTECTED SAMPLE')
    c.card(915,717,350,185,'MA-EXP-03','Established camp → Rootstone → observation and return. Second yard optional.','CONNECTED INVESTIGATION')
    c.text(56,968,'Arrows describe campaign relationships; they are not distances, depth bands or proof of drivable terrain.',20,MUTED)
    record(c,'c07-d05-mars-journeys.svg','Mars route relationships and three complete outings',['C07-MARS-04','C07-EXP1-01','C07-EXP2-01','C07-EXP3-01'],['MA-S1','MA-S2','MA-L1','MA-L2','MA-L3','MA-EXP-01','MA-EXP-02','MA-EXP-03'],'MA-EXP-01 completes a surface/gallery return; MA-EXP-02 follows a buried continuation to a protected brine sample; MA-EXP-03 builds on known routes for Rootstone observations. Each returns to a supported place and preserves earlier route value.','Five regions connect from surface terraces through delta and galleries to Brine Scarps and Rootstone. Three outing summaries identify distinct goals and shared return obligations.')

    c=Canvas('C07-D06',"The River That Isn't There",'Progress follows retained observations and useful route knowledge. A disproved branch is information, not a campaign reset.',1220)
    steps=[(55,190,'Read surface branching','A viewpoint reveals channels and a buried continuation.','MA-E01'),(480,190,'Compare candidates','Observe an exposure; record why a branch fits or fails.','MA-E02'),(905,190,'Disproved continuation','Retain the observation and any useful route. Reconsider another candidate.','VALID WRONG TURN'),(480,435,'Establish a field camp','A supported stop allows protected work and a dependable retreat.','MA-E03'),(55,650,'Follow mineral interfaces','Use imported protection for the first localized brine evidence.','MA-E04'),(480,650,'Trace Rootstone exposures','Compare separated structures instead of cutting blindly through a rib.','MA-E05'),(905,650,'Commission observation','Choose a supported instrument position; qualify a poor reading.','MA-E06'),(480,925,'Make the knowledge useful','Preserve the route, evidence and practical return value. A large yard remains optional.','MA-E07')]
    for x,y,label,body,tag in steps:c.card(x,y,360,170,label,body,tag)
    c.arrow([(415,272),(472,272)]);c.arrow([(840,272),(897,272)],'reject',849,252)
    c.arrow([(1085,360),(1085,392),(660,392),(660,362)],dashed=True)
    c.arrow([(660,360),(660,427)])
    c.arrow([(660,605),(660,626),(235,626),(235,641)])
    c.arrow([(415,730),(472,730)]);c.arrow([(840,730),(897,730)])
    c.arrow([(1085,820),(1085,876),(660,876),(660,916)])
    c.text(60,1150,'Evidence persists; consumed specimen stock does not. Optional CEC01 life is not a clue requirement.',19,MUTED)
    record(c,'c07-d06-mars-investigation.svg','Signature investigation: retained evidence and useful wrong turns',['C07-EVID-01','C07-EVID-02'],['MA-PLAN-07','MA-S1','MA-S2','MA-L1','MA-L2','MA-L3'],'The existing seven-step signature investigation links visible drainage, candidate comparison, field support, mineral interfaces and connected Rootstone exposures. A rejected candidate preserves evidence and allows reconsideration; it does not wipe earlier progress.','Seven evidence steps lead from surface pattern to useful route, with a side branch for disproving a candidate and returning to comparison without losing evidence.')

    c=Canvas('C07-D07','A Frontier Yard grows when it earns its place','Six existing stages describe useful additions. They are not a compulsory research ladder or a requirement to build a city.',1070)
    c.card(395,190,530,140,'Landing pad and refuge','Commission actual support with a clear arrival, emergency approach and real stock.','FY01 · FIRST USEFUL STOP')
    for x in [52,468,884]:c.arrow([(660,330),(660,368),(x+192,368),(x+192,405)])
    c.card(52,415,384,190,'Rover bay','Compatible routine repair, secure load and a clear boarding/service path.','FY02 · VEHICLE USE')
    c.card(468,415,384,190,'Weather and map room','Read actual observations and prepare a more dependable route.','FY03 · KNOWLEDGE')
    c.card(884,415,384,190,'Materials workshop','A selected local process with real containment, energy and receiver stock.','FY04 · USEFUL PROCESS')
    c.card(245,715,384,175,'Greenhouse','Optional cultivation with its own actual enclosure and service support.','FY05 · OPTIONAL DAILY LIFE')
    c.card(690,715,384,175,'Regional depot','Optional storage and receiving for already-produced, reserved real goods.','FY06 · OPTIONAL LOGISTICS')
    c.arrow([(660,330),(1285,330),(1285,650),(437,650),(437,706)],dashed=True)
    c.arrow([(1285,650),(882,650),(882,706)],dashed=True)
    c.note(925,'A small yard is a valid outcome','Each addition must earn its own supplies, access and purpose. Parked regions retain state without unattended production.')
    record(c,'c07-d07-frontier-yard.svg','Frontier Yard: useful stages and optional growth',['C07-YARD-02','C07-YARD-03'],['MA-PLAN-06','SUP01','IND-M58','IND-M51','IND-M56'],'A supported first refuge can acquire a rover bay, observation space and a selected material process as they become useful. Greenhouse and depot remain optional additions with their own actual services. Dashed links indicate optional growth from the established yard, not sufficient engineering dependencies.','A first landing/refuge branches to vehicle, knowledge and process additions; dashed optional branches reach a greenhouse and a regional depot.')

    c=Canvas('C07-D08','Two Mars discoveries, two bounded improvements','First arrival uses earlier reachable equipment and supplies. These products reward a successful return instead of enabling it retroactively.',1130)
    c.text(58,204,'PM03 · Ferric Lattice',25,COPPER,weight='700');c.text(702,204,'PM04 · Brine Catalyst',25,COPPER,weight='700')
    cols=[(52,'Accepted ferric feed','Retained specimen, committed production and remaining reserve are separate quantities.','Manufacture a supported member','Selected forming/composite method uses actual companion inputs. Recipe remains unselected.','Replace one exterior rack corner','Return the old member to one defined disposition. Earlier compatible stock remains valid.'),
          (696,'Protected mineral / brine sample','Imported sampler obtains the first stock. Study does not create a second material copy.','Prepare a finite active charge','Selected sample/process duty fills a reusable housing; carrier stock and services are real.','Use, exhaust and replace the charge','Spent active material remains spent. Cleaning the housing alone does not renew its duty.')]
    for x,l1,b1,l2,b2,l3,b3 in cols:
      c.card(x,240,572,180,l1,b1,'ACTUAL INPUT');c.arrow([(x+286,420),(x+286,467)])
      c.card(x,477,572,180,l2,b2,'DEFINED PROCESS');c.arrow([(x+286,657),(x+286,704)])
      c.card(x,714,572,180,l3,b3,'FIRST USEFUL APPLICATION')
    c.note(935,'Specific benefit; earlier alternative','PM03 does not automatically add cargo slots. PM04 does not provide perpetual supply. Neither requires optional Mars mats.')
    record(c,'c07-d08-mars-products.svg','PM03 and PM04: accountable first applications',['C07-YARD-05','C07-YARD-06','C07-ORDER-03'],['PM03','PM04','IND-M23','IND-M51','IND-M58','SUP01'],'The first PM03 example is manufactured replacement geometry for one existing rover member. The PM04 example is a finite active charge in reusable housing. Both retain actual input custody, a defined exhausted/removed disposition and an earlier supported alternative.','Two parallel material routes show actual input, defined processing and a bounded application, preserving specimens and accounting for spent or removed material.')
    (HERE/'diagrams_07.json').write_text(json.dumps({'edition':'workshop-mars-07','figures':figs},ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
    print(f'Wrote {len(figs)} editable SVG diagrams.')

if __name__=='__main__':main()
