"""Render the editorial companion and its actual available visual coverage."""
from pathlib import Path
import json
import re
import hashlib
import shutil
from collections import Counter


def read_records(path):
    if not path.exists():
        return []
    data = json.loads(path.read_text())
    if isinstance(data, list):
        return data
    for key in ('records', 'diagrams', 'sheets'):
        if key in data:
            return data[key]
    raise ValueError('No figure records in ' + str(path))


class IllustratedCompanion:
    def __init__(self, root, out, esc, inline, badge, anchor, search, art, old_diagrams):
        self.root, self.out = Path(root), Path(out)
        self.esc, self.inline, self.badge, self.anchor, self.search = esc, inline, badge, anchor, search
        self.portable = self.root.name == 'sources'
        self.data = json.loads((self.root/'illustrated_editorial.json').read_text())
        self.slices = self.data['slices']
        self.guides = {g['title']: g for g in self.data['chapter_guides']}
        self.art, self.old_diagrams = art, old_diagrams
        self.boards = read_records(self.root/'illustrated_03_boards.json')
        self.diagrams = read_records(self.root/'illustrated_03_diagrams.json')
        self.interfaces = read_records(self.root/'illustrated_03_interfaces.json')
        self.figures = {}
        for kind, records, folder in [('concept', self.boards, 'illustrated_03_assets'), ('diagram', self.diagrams, 'illustrated_03_diagrams'), ('interface', self.interfaces, 'illustrated_03_interfaces')]:
            for original in records:
                rec = dict(original)
                rec['kind'] = kind
                rec['file'] = Path(rec['file']).name
                rec['source_path'] = str(self.out/('images/originals' if kind == 'concept' else 'images/diagrams')/rec['file']) if self.portable else str(self.root/folder/rec['file'])
                rec['target'] = 'illustrated-figure-' + rec['id'].lower()
                rec['available'] = Path(rec['source_path']).is_file()
                self.figures[rec['id']] = rec
        self._catalogue = None

    def slice_target(self, number):
        return self.anchor('ILL-S' + str(number).zfill(2))

    def ready(self, s):
        return all(self.figures.get(i, {}).get('available') for i in s['figures'])

    def copy_extra_assets(self):
        def copy(source, target):
            source, target = Path(source), Path(target)
            if source.resolve() != target.resolve():
                shutil.copy2(source, target)
        for rec in self.figures.values():
            if rec['kind'] != 'concept' and rec['available']:
                copy(rec['source_path'], self.out/'images/diagrams'/rec['file'])
        for filename in ('illustrated_editorial.json', 'illustrated_03_scope_data.json', 'illustrated_03_boards.json', 'illustrated_03_diagrams.json', 'illustrated_03_interfaces.json', 'planning-edition-02-manifest.json'):
            if (self.root/filename).is_file():
                copy(self.root/filename, self.out/'sources'/filename)
        for filename in ('illustrated_companion.py', 'build_illustrated_editorial.py', 'build_illustrated_diagrams.py', 'build_interface_sheets.py'):
            if (self.root/filename).is_file():
                copy(self.root/filename, self.out/'sources'/filename)

    def old_target(self, ident):
        if ident.startswith('REF'):
            return 'ref-' + ident.lower()
        if ident in ('EAR01_H', 'EAR03_H', 'EAR05_H'):
            return 'art-' + ident.lower().replace('_','-')
        if ident.startswith('OV-'):
            return 'art-' + ident.lower()
        return 'diagram-' + ident

    def context_link(self, ident):
        if ident in self.art:
            title = self.art[ident]['name']
        else:
            record = next((d for d in self.old_diagrams if Path(d['file']).stem == ident), None)
            title = record['title'] if record else ident
        return '<a href="#' + self.old_target(ident) + '"><span>' + self.esc(ident) + '</span>' + self.esc(title) + ' →</a>'

    def intro(self):
        completed = sum(self.ready(s) for s in self.slices)
        h = '<section class="section-wrap illustrated-hub" id="illustrated-overview"><div class="section-head"><div><span class="eyebrow">Illustrated overview 03 / Start here</span><h2>A field guide to the whole journey.</h2></div><p>Choose a place, follow a process, or inspect an object. These ten reading guides connect the complete writing to concept boards, exact diagrams, and interface references.</p></div>'
        h += '<div class="illustrated-stats"><div><b>' + str(completed) + ' / 10</b><span>document slices illustrated</span></div><div><b>88,193</b><span>earlier source words preserved</span></div><div><b>' + str(len(self.boards)) + ' / 8</b><span>new concept boards</span></div><div><b>' + str(len(self.diagrams) + len(self.interfaces)) + '</b><span>new diagrams and interface sheets</span></div></div>'
        h += '<p class="overview-intro">The full text and original artwork remain available. A board introduces representative appearance and activity; a diagram explains a relationship; a supplied reference anchors an established design. Dedicated variants and production views remain individually tracked.</p>'
        h += '<div class="illustrated-slice-grid">'
        for s in self.slices:
            target = self.slice_target(s['number'])
            h += '<a class="illustrated-slice-card" href="#' + target + '"><span class="card-id">' + str(s['number']).zfill(2) + ' / ' + self.esc(s['id']) + '</span><h3>' + self.esc(s['title']) + '</h3><p>' + self.esc(s['summary']) + '</p>' + self.badge('Overview complete' if self.ready(s) else 'In production', 'confirmed' if self.ready(s) else 'new') + '<span class="card-arrow" aria-hidden="true">↗</span></a>'
        h += '</div><div class="hero-links"><a class="button" href="#illustrated-coverage">Coverage by slice</a><a class="button" href="#visual-directory">All illustrations and diagrams</a><a class="button" href="#chapter-directory">Find a chapter</a></div></section>'
        return h

    def chapter_directory(self):
        h = '<section class="section-wrap" id="chapter-directory"><div class="section-head"><div><span class="eyebrow">Reading directory</span><h2>Find the question you want to explore.</h2></div><p>Every earlier chapter has a concise guide and a route into the illustrated companion. The underlying writing is preserved in full.</p></div><div class="backlog-controls"><input id="chapter-guide-filter" type="search" placeholder="Find a world, system, or chapter…" aria-label="Filter chapter guides"><select id="chapter-guide-slice" aria-label="Filter chapter guides by illustrated slice"><option value="">All reading guides</option>'
        h += ''.join('<option value="' + str(s['number']) + '">' + str(s['number']).zfill(2) + ' · ' + self.esc(s['title']) + '</option>' for s in self.slices)
        h += '</select></div><p id="chapter-guide-count" class="backlog-count" aria-live="polite"></p><div class="chapter-guide-grid">'
        for title, g in self.guides.items():
            h += '<article class="card chapter-guide-card" data-chapter-guide data-slice="' + str(g['slice']) + '"><h3><a href="#' + self.anchor(title) + '">' + self.esc(title) + '</a></h3><p>' + self.esc(g['summary']) + '</p><a class="guide-context" href="#' + self.slice_target(g['slice']) + '">Illustrated reading guide →</a></article>'
        return h + '</div></section>'

    def chapter_note(self, title):
        g = self.guides.get(title)
        if not g:
            return ''
        return '<aside class="chapter-guide-note"><span class="eyebrow">What this chapter covers</span><p>' + self.esc(g['summary']) + '</p><a href="#' + self.slice_target(g['slice']) + '">Open its illustrated guide →</a></aside>'

    def coverage(self):
        h = '<section class="section-wrap" id="illustrated-coverage"><div class="section-head"><div><span class="eyebrow">Coverage / Ten document slices</span><h2>What this edition makes visible.</h2></div><p>Overview completion describes the illustrated reference. It does not erase the dedicated-view backlog or imply that a concept image is a finished game asset.</p></div><div class="table-wrap" tabindex="0" aria-label="Document slice visual coverage"><table><thead><tr><th>Slice</th><th>Coverage in this edition</th><th>Further dedicated views</th></tr></thead><tbody>'
        for s in self.slices:
            h += '<tr><td><a href="#' + self.slice_target(s['number']) + '">' + str(s['number']).zfill(2) + ' · ' + self.esc(s['title']) + '</a><br>' + self.badge('Overview complete' if self.ready(s) else 'In production', 'confirmed' if self.ready(s) else 'new') + '</td><td>' + self.esc(s['overview_result']) + '</td><td>' + self.esc(s['still_dedicated']) + '</td></tr>'
        h += '</tbody></table></div><div class="intro-note"><strong>One coherent overview; a larger illustration programme.</strong> The earlier 1,076-record commissioning register remains intact. Its dedicated model, regional, and state views are not automatically completed by a representative board. This edition adds a separate current visual catalogue, with each new board identifying the subjects it actually depicts.</div></section>'
        return h

    def catalogue(self):
        if self._catalogue is not None:
            return self._catalogue
        records = []
        new_ids = set(self.figures)
        for ident, a in self.art.items():
            if ident in new_ids:
                continue
            records.append({'id':ident, 'title':a['name'], 'kind':'Confirmed reference' if ident.startswith('REF') else 'Retained concept', 'status':'Included before this edition', 'target':self.old_target(ident), 'caption':a['caption'], 'source_ids':[ident]})
        for d in self.old_diagrams:
            stem = Path(d['file']).stem
            records.append({'id':'SVG-' + stem, 'title':d['title'], 'kind':'Retained diagram', 'status':'Included before this edition', 'target':'diagram-' + stem, 'caption':d['alt'], 'source_ids':[], 'file':'images/diagrams/' + d['file']})
        for rec in self.figures.values():
            records.append({'id':rec['id'], 'title':rec['title'], 'kind':{'concept':'New concept board','diagram':'New diagram','interface':'New interface sheet'}[rec['kind']], 'status':'Included' if rec['available'] else 'In production', 'target':rec['target'], 'caption':rec.get('caption',rec.get('alt','')), 'source_ids':rec.get('source_ids',[]), 'coverage_note':rec.get('coverage_note','Precise documentation illustration; stated design proposals remain proposals.'), 'file':('images/' + rec['id'].lower() + '.webp') if rec['kind']=='concept' else ('images/diagrams/' + rec['file'])})
        self._catalogue = records
        return records

    def visual_directory(self):
        records = self.catalogue()
        kinds = list(dict.fromkeys(r['kind'] for r in records))
        h = '<section class="section-wrap" id="visual-directory"><div class="section-head"><div><span class="eyebrow">Visual index</span><h2>Find an illustration by its job.</h2></div><p>Concepts, confirmed references, exact diagrams, and interface sheets are indexed separately. Search includes their captions and source identifiers.</p></div><div class="backlog-controls"><input id="visual-filter" type="search" placeholder="Find an image, diagram, caption, or source ID…" aria-label="Filter the visual catalogue"><select id="visual-kind" aria-label="Visual kind"><option value="">All visual kinds</option>'
        h += ''.join('<option value="' + self.esc(k) + '">' + self.esc(k) + '</option>' for k in kinds)
        h += '</select><button id="export-visuals" type="button">Export visual catalogue</button></div><p id="visual-count" class="backlog-count" aria-live="polite"></p><div class="table-wrap" tabindex="0" aria-label="Illustration and diagram catalogue"><table><thead><tr><th>Visual</th><th>Kind</th><th>What it explains</th><th>Source context</th></tr></thead><tbody>'
        for r in records:
            h += '<tr id="visual-row-' + r['id'].lower() + '" data-visual-row data-kind="' + self.esc(r['kind']) + '"><td><code>' + self.esc(r['id']) + '</code><br><a href="#' + r['target'] + '">' + self.esc(r['title']) + '</a></td><td>' + self.esc(r['kind']) + '<br>' + self.badge(r['status'],'confirmed' if r['status'].startswith('Included') else 'new') + '</td><td>' + self.esc(r['caption']) + '</td><td>' + self.esc(', '.join(r.get('source_ids',[]))) + '</td></tr>'
        h += '</tbody></table></div></section>'
        return h

    def figure(self, ident):
        rec = self.figures.get(ident)
        if not rec or not rec['available']:
            return '<div class="intro-note"><strong>' + self.esc(ident) + '</strong> · Overview visual in production.</div>'
        title = self.esc(rec['title'])
        caption = self.esc(rec.get('caption',rec.get('alt','')))
        if rec['kind'] == 'concept':
            web = 'images/' + ident.lower() + '.webp'
            original = 'images/originals/' + ident.lower() + '.png'
            h = '<figure class="plate companion-figure" id="' + rec['target'] + '"><button type="button" data-image="' + web + '" data-original="' + original + '" data-caption="' + title + '" aria-label="Enlarge ' + title + '"><img src="' + web + '" alt="' + self.esc(rec.get('alt',rec['title'])) + '" loading="lazy" decoding="async"></button><figcaption><div class="plate-meta"><strong>' + title + '</strong><span>' + ident + ' · Concept board</span></div>' + caption
        else:
            svg = Path(rec['source_path']).read_text()
            svg = re.sub(r'<\?xml[^>]*>', '', svg)
            prefix = ident.lower() + '-'
            ids = re.findall(r'\bid="([^"]+)"',svg)
            for sid in sorted(ids,key=len,reverse=True):
                newid = prefix + sid
                svg = svg.replace('id="' + sid + '"','id="' + newid + '"').replace('url(#' + sid + ')','url(#' + newid + ')').replace('href="#' + sid + '"','href="#' + newid + '"')
                svg = re.sub(r'(aria-labelledby=")([^"]+)(")',lambda m: m[1] + ' '.join(newid if v==sid else v for v in m[2].split()) + m[3],svg)
            h = '<figure class="diagram companion-figure" id="' + rec['target'] + '">' + svg + '<figcaption><strong>' + title + '</strong><br>' + caption + ' <a href="images/diagrams/' + rec['file'] + '" target="_blank">Open full-size diagram</a>'
        if rec.get('coverage_note'):
            h += '<p class="figure-coverage">' + self.esc(rec['coverage_note']) + '</p>'
        sources = rec.get('source_ids',[])
        if sources:
            h += '<p class="figure-sources"><strong>Source context:</strong> ' + ' · '.join('<a href="#' + self.anchor(i) + '">' + self.esc(i) + '</a>' if not i.startswith('REF') else '<a href="#ref-' + i.lower() + '">' + self.esc(i) + '</a>' for i in sources) + '</p>'
        h += '</figcaption></figure>'
        self.search.append({'id':rec['target'],'chapter':'Illustrated overview · Visual references','title':ident + ' · ' + rec['title'],'text':' '.join([rec['title'],rec.get('caption',''),rec.get('alt',''),rec.get('coverage_note',''),' '.join(sources)]),'source':'illustrated-visuals'})
        return h

    def _table(self, headers, rows, label):
        return '<div class="table-wrap" tabindex="0" aria-label="' + self.esc(label) + '"><table><thead><tr>' + ''.join('<th>' + self.esc(h) + '</th>' for h in headers) + '</tr></thead><tbody>' + ''.join('<tr>' + ''.join('<td>' + c + '</td>' for c in row) + '</tr>' for row in rows) + '</tbody></table></div>'

    def slice_insert(self, number):
        s = next(s for s in self.slices if s['number']==number)
        h = '<div class="illustrated-slice-insert" data-illustrated-slice="' + str(number) + '">'
        if number == 1:
            rows = [
                ['Complete direction', 'Read the preserved prose for the full intended experience.'],
                ['Confirmed reference', 'Use the supplied sheets as the established appearance reference.'],
                ['Concept board', 'Inspect representative silhouettes, atmosphere, material language, and activity.'],
                ['Diagram or interface sheet', 'Follow the exact documented relationships and labelled example states.'],
                ['Dedicated-view backlog', 'See the specific variants, states, and production views that remain future illustration work.']]
            h += self._table(['Reference type','How to use it'],[[self.esc(c) for c in row] for row in rows],'How to read this reference')
        elif number == 4:
            rows = [[self.esc(r['realm']),self.esc(' · '.join(r['experiences'])),self.esc(r['experience']),'<a href="#' + self.old_target(r['art']) + '">' + self.esc(r['art']) + '</a>'] for r in self.data['earth_regions']]
            h += self._table(['Realm','Five named experiences','What to notice','Retained art'],rows,'Earth realm and experience comparison')
        elif number in (5,6):
            dest = self.data['destinations'][:2] if number==5 else self.data['destinations'][2:]
            rows = []
            for r in dest:
                world = '<a href="#'+self.anchor(r['name'])+'"><strong>'+self.esc(r['name'])+'</strong></a><br>'+self.esc(r['role'])
                regions = '<br>'.join(self.esc(x['name']) for x in r['regions'])
                materials = '<br>'.join('<a href="#'+self.anchor(x['id'])+'">'+self.esc(x['id']+' · '+x['name'])+'</a>' for x in r['materials'])
                project = '<strong>'+self.esc(r['signature_project']['name'])+'</strong><br>'+self.esc(r['signature_project']['description'])+'<br><br>'+self.esc(' · '.join(r['controller_projects']))+'<br><br><a href="#'+self.old_target(r['art'])+'">'+self.esc(r['art'])+' · Retained concept</a>'
                rows.append([world,regions,materials,project])
            h += self._table(['Destination and journey role','Named regional experiences','Local material pair','Signature project and supporting places'],rows,'Destination experience and material comparison')
            if number==6:
                h += '<div class="intro-note"><strong>Other represented planets.</strong> Mercury, Venus, Uranus, and Neptune retain further destination-design backlog status. Jupiter and Saturn provide orbital scenery and access context. Their presence on the Solar System atlas does not add a conventional landing campaign.</div>'
        elif number==7:
            reg = json.loads((self.root/'industry_progression_register.json').read_text())
            rows = [[self.esc(b['id']+' · '+b['title']),self.esc(b['capability']),self.esc(b['first_result']),self.esc(b['exploration_relationship'])] for b in reg['industrial_bands']]
            h += self._table(['Capability band','What it provides','Useful outcome','Exploration relationship'],rows,'Industrial capability comparison')
        elif number==8:
            rows = [('Earth resources','19 families','Recognizable sources and local gathering stories','Materials and the crafting economy'),('Planetary materials','18 entries','Local products that strengthen later activity','Planetary materials and reference catalogue'),('Processed products','10 families','Useful intermediate appearances and work','Materials and the crafting economy'),('Equipment','32 families','Excavation, movement, protection, navigation, and preparation','Tools equipment and preparation')]
            h += self._table(['Catalogue','Written scope','Visual purpose','Full reference'],[[self.esc(a),self.esc(b),self.esc(c),'<a href="#'+self.anchor(d)+'">'+self.esc(d)+'</a>'] for a,b,c,d in rows],'Resource and equipment catalogue scope')
        elif number==9:
            rows = [('Earth creatures','12 families','Silhouette, scale, habitat, behaviour, and response'),('Earth plants and fungi','10 families','Habitat, growth form, harvestable part, and cultivation'),('Planetary ecology','11 proposed families','World-specific form and behaviour; dedicated visual views remain tracked. The Mars brine mat remains conditional.'),('Signature encounters','4 proposals','Readable signals, activity, and the intended player response'),('Residents','8 role references','Useful roles, recognizable routines, and settlement context')]
            h += self._table(['Family','Written scope','What a visual should explain'],[[self.esc(c) for c in row] for row in rows],'Living-world visual scope')
            h += '<div class="intro-note"><strong>Planetary ecology retains its own scope.</strong> The eleven entries below are proposed fictional families. Their individual anatomy and activity views remain dedicated illustration work. The optional Mars brine mat is not assumed present in every Mars image. Moon, Ceres, Io, Pluto, and Cairn do not acquire invented indigenous creature populations through this overview.</div>'
            ecology_rows = []
            for e in self.data['planetary_ecology']:
                ecology_rows.append([self.esc(e['world']),'<a href="#'+self.anchor(e['id'])+'">'+self.esc(e['id']+' · '+e['name'])+'</a>',self.esc(e['activity']),self.esc('Conditional; dedicated views remain' if e['conditional'] else 'Proposed family; dedicated views remain')])
            h += self._table(['World','Ecology family','Characteristic activity','Illustration status'],ecology_rows,'Planetary ecology reference and illustration scope')
        elif number==10:
            refs = [('REF09','Assembly station','A broader station controls construction in a linked vehicle berth.'),('REF10','Part fabrication','The smaller work area prepares components for later assembly.'),('REF11','Explorer component context','Exploded parts, underside, and hatch references inform the component relationship.'),('REF12','Suit and equipment','The supplied suit sheet anchors wearable appearance and the breathing-pack relationship.'),('REF15','The complete Tier 1 family','The named systems and Earth–Moon–Earth context connect hardware to the mission.')]
            h += self._table(['Confirmed sheet','Read it for','Document annotation'],[['<a href="#ref-'+r.lower()+'">'+r+'</a>',self.esc(t),self.esc(n)] for r,t,n in refs],'Confirmed Tier 1 reference annotations')
        h += ''.join(self.figure(i) for i in s['figures'])
        if s['existing_visuals']:
            h += '<div class="related-visuals"><h4>Retained illustrations and diagrams</h4><div>' + ''.join(self.context_link(i) for i in s['existing_visuals']) + '</div></div>'
        h += '<div class="related-chapters"><h4>Read the complete direction</h4>' + ' · '.join('<a href="#'+self.anchor(t)+'">'+self.esc(t)+'</a>' for t in s['chapters']) + '</div></div>'
        return h

    def write_records(self):
        catalogue = {'edition':'Illustrated overview 03','records':self.catalogue(),'unique_raster_concepts_and_references':len(self.art),'editable_figures':len(self.old_diagrams)+len(self.diagrams)+len(self.interfaces),'notes':['Display WebPs are copies of original raster images, not additional concepts.','All earlier artwork and text sources remain preserved.','Dedicated view backlog is separate from representative overview coverage.']}
        coverage = {'edition':'Illustrated overview 03','scope':'Document website only','slices':[{'id':s['id'],'title':s['title'],'status':'Overview complete' if self.ready(s) else 'In production','included_figures':s['figures'],'retained_visuals':s['existing_visuals'],'overview_result':s['overview_result'],'dedicated_views_remaining':s['still_dedicated']} for s in self.slices],'new_concept_board_cap':8,'new_concept_boards':len(self.boards),'new_drawn_figures':len(self.diagrams)+len(self.interfaces),'new_interface_views':12 if len(self.interfaces)==4 else None,'prior_source_words_preserved':88193,'earlier_commissioning_register_preserved':True}
        for name,data in [('illustrated_visual_catalogue.json',catalogue),('illustrated_coverage.json',coverage)]:
            (self.out/'sources'/name).write_text(json.dumps(data,ensure_ascii=False,indent=2))
            if not self.portable:
                (self.root/name).write_text(json.dumps(data,ensure_ascii=False,indent=2))
        return catalogue,coverage
