"""Build a readable offline view of exact selected CC BY source content."""
import argparse
from html import escape
from pathlib import Path
import xml.etree.ElementTree as ET

BASE = Path(__file__).resolve().parents[1]
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--source-dir', type=Path, default=BASE / 'data/sources')
parser.add_argument('--out', type=Path)
args = parser.parse_args()
out = args.out or args.source_dir / 'source-extracts.html'

ALLOWED = {'p', 'table', 'thead', 'tbody', 'tfoot', 'tr', 'td', 'th', 'sub', 'sup', 'strong', 'em', 'br'}
RENAME = {'bold': 'strong', 'italic': 'em', 'break': 'br'}
XLINK = '{http://www.w3.org/1999/xlink}href'

def plain(element):
    return ''.join(element.itertext()).strip()

def render(element, source_url):
    """Preserve source characters/order and chemical sub/superscripts, not JATS styles."""
    tag = RENAME.get(element.tag, element.tag)
    inner = escape(element.text or '') + ''.join(render(c, source_url) + escape(c.tail or '') for c in element)
    if tag == 'license':
        # The nested ALI license_ref and license-p are distinct metadata blocks.
        inner = escape(element.text or '') + ' '.join(render(c, source_url) + escape(c.tail or '') for c in element)
    if tag == 'xref':
        return '<a href="' + escape(source_url + '#' + element.get('rid', ''), quote=True) + '">' + inner + '</a>'
    if tag == 'ext-link':
        url = element.get(XLINK, '')
        return '<a href="' + escape(url, quote=True) + '">' + inner + '</a>' if url.startswith(('http://', 'https://')) else inner
    if tag not in ALLOWED:
        return inner
    attrs = ''.join(' ' + a + '="' + escape(element.get(a), quote=True) + '"' for a in ('rowspan', 'colspan') if a in element.attrib) if tag in {'th', 'td'} else ''
    return '<' + tag + attrs + '>' + ('' if tag == 'br' else inner + '</' + tag + '>')

def table(tree, table_id, source_url, anchor):
    wrap = tree.find('.//table-wrap[@id="' + table_id + '"]')
    heading = plain(wrap.find('label'))
    caption = render(wrap.find('caption'), source_url)
    body = render(wrap.find('table'), source_url)
    foot = wrap.find('table-wrap-foot')
    return ('<section id="' + anchor + '"><h3>' + escape(heading) + '</h3>' + caption
            + '<div class="table-scroll" tabindex="0">' + body + '</div>'
            + (render(foot, source_url) if foot is not None else '') + '</section>')

def paragraphs(tree, section_id, source_url, anchor, predicate=None, title=None):
    sec = tree.find('.//sec[@id="' + section_id + '"]')
    ps = [p for p in sec.findall('p') if predicate is None or predicate(plain(p))]
    heading = title or plain(sec.find('title'))
    return ('<section id="' + anchor + '"><h3>' + escape(heading) + '</h3>'
            + '<p class="locator">XML section: <code>' + escape(section_id) + '</code>. Complete selected paragraphs follow.</p>'
            + ''.join(render(p, source_url) for p in ps) + '</section>')

def attribution(tree, source_url, doi):
    meta = tree.find('.//article-meta')
    authors = []
    for group in meta.findall('contrib-group'):
        if group.get('content-type') == 'editor':
            continue
        for contributor in group.findall('contrib'):
            if contributor.get('contrib-type', 'author') != 'author':
                continue
            name = contributor.find('name')
            if name is not None:
                authors.append(plain(name.find('given-names')) + ' ' + plain(name.find('surname')))
    # Permissions children are separate metadata fields, not one prose sentence.
    # Keep boundaries visible so a license URL cannot concatenate with "Licensee".
    permissions = ' '.join(render(child, source_url) for child in meta.find('permissions'))
    return ('<p>' + escape('; '.join(authors)) + '.</p><p><a href="https://doi.org/' + doi + '">DOI: ' + doi
            + '</a> · <a href="' + source_url + '">Full source article</a></p>'
            + '<div class="license">' + permissions + '</div>')

L = ET.parse(args.source_dir / 'PMC6539611.xml')
K = ET.parse(args.source_dir / 'PMC9331396.xml')
LU = 'https://pmc.ncbi.nlm.nih.gov/articles/PMC6539611/'
KU = 'https://pmc.ncbi.nlm.nih.gov/articles/PMC9331396/'

body = '''<h1>Readable source extracts / Читаемые фрагменты первоисточников</h1>
<p>This offline document contains original source tables and complete selected paragraphs. Source wording and chemical sub/superscripts are preserved; navigation, typography and selection are added by LigninQC. It is not a corrected edition. The two conflicting ABTS statements and thermal sample assignments remain visible in their original contexts.</p>
<p>Этот документ показывает исходные таблицы и полные выбранные абзацы статей. Противоречащие друг другу значения сохранены; расчётные выводы находятся в отдельном отчёте.</p>
<nav><a href="#lauberte-table1">Lauberte Table 1</a><a href="#lauberte-regression">Reported R²</a><a href="#lauberte-abts-prose">ABTS prose 5/6</a><a href="#lauberte-states">Chemical states</a><a href="#lauberte-assays">Assay methods</a><a href="#kazachenko-table2">Kazachenko Table 2</a><a href="#kazachenko-thermal-prose">Thermal prose</a></nav>
<p class="locator">Offline primary sources: <a href="PMC6539611.xml">PMC6539611.xml</a> and <a href="PMC9331396.xml">PMC9331396.xml</a>. Cross-reference links inside original paragraphs open the full source online; the selected content below needs no network.</p>
<article id="lauberte"><h2>Lauberte et al., Molecules 2019, 24, 1794</h2>'''
body += attribution(L, LU, '10.3390/molecules24091794')
body += '<h3>' + escape(plain(L.find('.//article-meta/title-group/article-title'))) + '</h3>'
body += table(L, 'molecules-24-01794-t001', LU, 'lauberte-table1')
body += paragraphs(L, 'sec2-molecules-24-01794', LU, 'lauberte-regression', lambda p: '0.63' in p, '2. Results and Discussion: reported regressions')
body += paragraphs(L, 'sec2dot2-molecules-24-01794', LU, 'lauberte-abts-prose', lambda p: '1.45 and 1.44' in p, '2.2. ABTS values in prose for compounds 5 and 6')
body += '<details><summary>Original structures and chemical-state discussion</summary>'
body += '<section id="lauberte-structures"><h3>Figure 1. Chemical structures of the lignin model compounds.</h3><img src="lauberte2019-figure1.png" alt="Original Figure 1: structures of compounds 1–19; phenolic OH groups can be inspected directly."></section>'
body += paragraphs(L, 'sec2dot4-molecules-24-01794', LU, 'lauberte-states')
body += paragraphs(L, 'sec2dot5-molecules-24-01794', LU, 'lauberte-states-gamma')
body += paragraphs(L, 'sec2dot2-molecules-24-01794', LU, 'lauberte-dimer16', lambda p: 'two phenolic OH' in p, '2.2. Full paragraph discussing compounds 5/6 and dimer 16')
body += paragraphs(L, 'sec2dot6-molecules-24-01794', LU, 'lauberte-dimers') + '</details>'
body += paragraphs(L, 'sec3dot3-molecules-24-01794', LU, 'lauberte-computation', lambda p: 'DFT calculations were performed' in p)
body += paragraphs(L, 'sec3dot4-molecules-24-01794', LU, 'lauberte-assays')
body += '</article><article id="kazachenko"><h2>Kazachenko et al., Polymers 2022, 14, 3000</h2>'
body += attribution(K, KU, '10.3390/polym14153000')
body += '<h3>' + escape(plain(K.find('.//article-meta/title-group/article-title'))) + '</h3>'
body += table(K, 'polymers-14-03000-t002', KU, 'kazachenko-table2')
body += paragraphs(K, 'sec3dot5-polymers-14-03000', KU, 'kazachenko-thermal-prose', lambda p: 'maximum weight loss' in p or '76.6 and 61.9' in p)
body += paragraphs(K, 'sec2-polymers-14-03000', KU, 'kazachenko-thermal-methods', lambda p: 'The thermal analysis included' in p, '2. Materials and Methods: thermal analysis')
body += '<details><summary>GPC table and full methods paragraph</summary>'
body += table(K, 'polymers-14-03000-t001', KU, 'kazachenko-gpc-table')
body += paragraphs(K, 'sec2-polymers-14-03000', KU, 'kazachenko-gpc-methods', lambda p: 'The weight average molecular weight' in p, '2. Materials and Methods: GPC')
body += '</details></article><footer><p>Source material: original authors, CC BY 4.0. Added selection and layout: LigninQC, 2026, CC BY 4.0. No endorsement by source authors is implied. Additional cited papers are not reproduced.</p></footer>'

css = '''*{box-sizing:border-box}body{font:17px/1.6 system-ui,sans-serif;color:#18313c;background:#fafbf9;margin:0}main{max-width:1120px;margin:auto;padding:32px 22px 70px}h1{font-size:2rem;line-height:1.25}h2{border-top:2px solid #b9cccf;padding-top:24px;margin-top:44px}h3{line-height:1.3;margin-top:30px}p{max-width:90ch}a{color:#005e6c;text-underline-offset:3px}nav{display:flex;flex-wrap:wrap;gap:12px 20px;padding:18px;background:#e8f0ed}.locator,.license{font-size:.88rem;color:#486069}.license,a{overflow-wrap:anywhere}.license{border-left:3px solid #83aaa1;padding-left:14px}.table-scroll{overflow-x:auto}table{border-collapse:collapse;background:white;width:100%;font-size:.85rem}th,td{border-bottom:1px solid #d2dfe0;padding:10px 12px;text-align:left;white-space:normal}th{background:#e8eff0}img{max-width:100%;height:auto;background:white}summary{cursor:pointer;font-weight:650;padding:16px;background:#e8f0ed}details{margin:24px 0}section{scroll-margin-top:16px}footer{border-top:1px solid #ccd8dc;margin-top:40px;font-size:.9rem}a:focus-visible,summary:focus-visible{outline:3px solid #cb841f;outline-offset:3px}code{overflow-wrap:anywhere}@media(max-width:600px){main{padding:22px 14px}body{font-size:16px}th,td{padding:8px}}@media print{details{display:block}main{padding:0}nav{display:none}.table-scroll{overflow:visible}h2,h3{break-after:avoid}tr{break-inside:avoid}}'''
document = '<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>LigninQC — readable original source extracts</title><style>' + css + '</style></head><body><main>' + body + '</main></body></html>\n'
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(document, encoding='utf-8')
print(out)
