"""Offline HTML/PDF export through the already installed Chromium and CDP."""
import base64
import hashlib
import html
from html.parser import HTMLParser
import json
import os
from pathlib import Path
import re
import subprocess
import time
import urllib.request

import websocket
from content import PAGES, QUICK_IDS, COMMANDS, table, ref

RUN = Path(__file__).resolve().parent
OUT = Path('/workspace/architecture_docs/topics/contextra/20260930_field_operations_gamebook')
REPO = Path('/workspace/contextra-analytics-runtime-lifecycle-refactor')
CHROME = '/root/.cache/ms-playwright/chromium_headless_shell-1228/chrome-headless-shell-linux64/chrome-headless-shell'
FULL = '01_contextra_field_gamebook_20260930'
QUICK = '02_contextra_field_quick_20260930'
NUMBER = {p['id']: i+1 for i,p in enumerate(PAGES)}
assert len(NUMBER) == len(PAGES)

CSS = '''
@page { size: A4; margin: 0; }
* { box-sizing: border-box; letter-spacing: 0; }
html, body { margin: 0; padding: 0; color: #17232a; background: #fff; }
body { font-family: "Noto Sans CJK KR", sans-serif; font-size: 11.2pt; line-height: 1.48; }
.page { width: 210mm; height: 297mm; padding: 14mm 16mm 18mm; position: relative; break-after: page; }
.page:last-child { break-after: auto; }
.kicker { display: flex; justify-content: space-between; border-bottom: 1.5pt solid #263e43; padding-bottom: 6px; font-size: 9pt; color: #435157; }
.category { margin: 12px 0 3px; font-size: 10pt; color: #435157; }
h1 { margin: 0 0 9px; font-size: 20pt; line-height: 1.25; font-weight: 700; }
.intro { margin: 0 0 13px; border-left: 3px solid #68797d; padding-left: 10px; }
h2 { font-size: 12.3pt; line-height: 1.35; margin: 14px 0 7px; font-weight: 700; }
p { margin: 7px 0; word-break: keep-all; overflow-wrap: anywhere; }
table { width: 100%; border-collapse: collapse; table-layout: fixed; margin: 10px 0; font-size: 11pt; line-height: 1.42; }
th { background: #edf1f1; color: #1c3437; font-weight: 700; }
td, th { border: 0.6pt solid #9aa5a8; padding: 6px 8px; text-align: left; vertical-align: top; overflow-wrap: anywhere; word-break: keep-all; }
.command { margin: 9px 0; border: 0.7pt solid #758589; background: #f5f6f6; break-inside: avoid; }
.where { padding: 4px 9px; border-bottom: 0.5pt solid #b5bfc2; color: #435157; font-size: 9pt; }
pre { margin: 0; padding: 8px 9px; font: 10.7pt/1.45 "DejaVu Sans Mono", "Noto Sans CJK KR", monospace; white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word; }
code { font-family: "DejaVu Sans Mono", "Noto Sans CJK KR", monospace; font-size: .92em; overflow-wrap: anywhere; }
.path { font-size: 10.2pt; }
kbd { font-family: "DejaVu Sans Mono", monospace; font-size: 10.5pt; font-weight: 700; border-bottom: 1px solid #777; }
aside { margin: 13px 0; padding: 10px 12px; border: 1pt solid #64777c; border-left: 4pt solid #455f65; break-inside: avoid; }
aside strong { font-size: 11.4pt; }
aside p { margin: 5px 0 0; }
a { color: #223b40; text-decoration: underline; }
.xref { white-space: nowrap; font-weight: 700; }
footer { position: absolute; left: 16mm; right: 16mm; bottom: 8mm; border-top: .6pt solid #9aa5a8; padding-top: 5px; display: flex; justify-content: space-between; font-size: 8.6pt; color: #435157; }
.fingerprint { font-family: "DejaVu Sans Mono", monospace; font-size: 9.5pt; overflow-wrap: anywhere; }
.flow { display: flex; align-items: center; gap: 9px; margin: 13px 0; }
.flow span { flex: 1; text-align: center; border: 1pt solid #607276; padding: 8px 4px; font-size: 11pt; }
.toc table { font-size: 11pt; line-height: 1.2; }
.toc td, .toc th { padding: 3px 7px; }
.toc .intro { margin-bottom: 8px; }
.toc h1 { font-size: 18pt; }
.toc .category { margin-top: 9px; }
@media screen { body { background: #e7ebec; } .page { margin: 15px auto; background: #fff; box-shadow: 0 1px 5px #bbb; } }
'''

def body_for(page):
    if page['id'] != 'G-01':
        return page['body']
    rows = [(q['id'], q['title'], ref(q['id'])) for q in PAGES if q['id'] != 'G-01']
    return table(['항목','상황 / 작업','쪽'], rows, [14,68,18])

def resolve_refs(body, included):
    def replace(match):
        key = match.group(1)
        assert key in NUMBER, key
        label = (key+' · ' if not re.search(r'^G-', key) else key+' · ') + str(NUMBER[key])+'쪽'
        if key not in included:
            return f'<span class="xref">{label} (통합본)</span>'
        return f'<a class="xref" href="#{key}">{label}</a>'
    return re.sub(r'<a class="xref" data-ref="([^"]+)">[^<]+</a>', replace, body)

def document(pages, quick=False):
    included = {p['id'] for p in pages}
    sections=[]
    for i,p in enumerate(pages):
        title=html.escape(p['title'])
        pagination=f'통합 {NUMBER[p["id"]]:02d} / {len(PAGES)}쪽'
        if quick: pagination += f' · 빠른본 {i+1}/{len(pages)}'
        sections.append(f'''<section class="page {'toc' if p['id']=='G-01' else ''}" id="{p['id']}">
        <div class="content"><div class="kicker"><span>CONTEXTRA · 현장 운영·복구</span><span>{p['id']} · {'빠른 대응본' if quick else '통합본'}</span></div>
        <div class="category">{html.escape(p['category'])}</div><h1>{title}</h1>
        <p class="intro">{p['situation']}</p><main>{resolve_refs(body_for(p),included)}</main></div>
        <footer><span>2026.09.30 준비 기준 · 기본 si · <a href="#G-00">처음으로</a></span><span>{pagination}</span></footer></section>''')
    title='Contextra 현장 운영·복구 '+('빠른 대응본' if quick else '통합 매뉴얼')
    return '<!doctype html><html lang="ko"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>'+title+'</title><style>'+CSS+'</style></head><body>'+''.join(sections)+'</body></html>'

CHECK = '''(() => {
 const errors=[];
 const pages=[...document.querySelectorAll('.page')];
 for (const p of pages) {
  const c=p.querySelector('.content').getBoundingClientRect(), f=p.querySelector('footer').getBoundingClientRect();
  if(c.bottom>f.top-9) errors.push({page:p.id,type:'footer-overlap',excess:c.bottom-f.top+9});
  for(const e of p.querySelectorAll('pre,td,th,h1,h2,p,aside,.flow,code')) {
   const r=e.getBoundingClientRect(), pr=p.getBoundingClientRect();
   if(r.right>pr.right-48 || r.left<pr.left+48 || e.scrollWidth>e.clientWidth+2 && getComputedStyle(e).display!='inline')
    errors.push({page:p.id,type:'horizontal',tag:e.tagName,excess:e.scrollWidth-e.clientWidth,text:e.innerText.slice(0,70)});
  }
  for(const a of p.querySelectorAll('a')) if(a.getAttribute('href').startsWith('#') && !document.getElementById(a.hash.slice(1))) errors.push({page:p.id,type:'link',href:a.href});
 }
 return {pages:pages.length,errors,externalResources:performance.getEntriesByType('resource').filter(x=>/^https?:/.test(x.name)).length};
})()'''

class CDP:
    def __init__(self, ws):
        self.ws=websocket.create_connection(ws,timeout=60,suppress_origin=True)
        self.seq=0
    def call(self,method,params=None):
        self.seq+=1; number=self.seq
        self.ws.send(json.dumps(dict(id=number,method=method,params=params or {})))
        while True:
            result=json.loads(self.ws.recv())
            if result.get('id')==number:
                if 'error' in result:raise RuntimeError(result['error'])
                return result.get('result',{})

def render():
    OUT.mkdir(parents=True,exist_ok=True)
    profile=RUN/'chrome-profile'
    (profile/'DevToolsActivePort').unlink(missing_ok=True)
    log=(RUN/'render.log').open('w')
    args=[CHROME,'--no-sandbox','--disable-gpu','--disable-background-networking','--no-first-run',
          '--disable-dev-shm-usage','--remote-debugging-address=127.0.0.1','--remote-debugging-port=0',
          '--user-data-dir='+str(profile),'about:blank']
    proc=subprocess.Popen(args,stdout=log,stderr=log)
    try:
        portfile=profile/'DevToolsActivePort'
        for _ in range(80):
            if portfile.exists():break
            if proc.poll() is not None:raise RuntimeError('Chromium exited')
            time.sleep(.1)
        port=portfile.read_text().splitlines()[0]
        targets=json.load(urllib.request.urlopen('http://127.0.0.1:'+port+'/json',timeout=5))
        page_target=next(t for t in targets if t['type']=='page')
        cdp=CDP(page_target['webSocketDebuggerUrl'])
        cdp.call('Page.enable');cdp.call('Runtime.enable');cdp.call('Network.enable')
        cdp.call('Network.setBlockedURLs',{'urls':['http://*','https://*']})
        cdp.call('Emulation.setEmulatedMedia',{'media':'print'})
        checks={}
        for name,pages,quick in [(FULL,PAGES,False),(QUICK,[p for p in PAGES if p['id'] in QUICK_IDS],True)]:
            source=OUT/(name+'.html');source.write_text(document(pages,quick),encoding='utf-8')
            cdp.call('Page.navigate',{'url':source.as_uri()})
            for _ in range(80):
                value=cdp.call('Runtime.evaluate',{'expression':'document.readyState','returnByValue':True})
                if value.get('result',{}).get('value')=='complete':break
                time.sleep(.1)
            cdp.call('Runtime.evaluate',{'expression':'document.fonts.ready.then(()=>true)','awaitPromise':True,'returnByValue':True})
            result=cdp.call('Runtime.evaluate',{'expression':CHECK,'returnByValue':True})['result']['value']
            checks[name]=result
            (RUN/'layout.json').write_text(json.dumps(checks,ensure_ascii=False,indent=2))
            assert not result['errors'],result
            assert not result['externalResources']
            pdf=cdp.call('Page.printToPDF',{'printBackground':True,'preferCSSPageSize':True,
                'displayHeaderFooter':False,'generateTaggedPDF':True,'generateDocumentOutline':True})
            (OUT/(name+'.pdf')).write_bytes(base64.b64decode(pdf['data']))
        cdp.ws.close()
    finally:
        proc.terminate()
        try:proc.wait(timeout=15)
        except subprocess.TimeoutExpired:proc.kill();proc.wait(timeout=5)
        log.close()
    (OUT/'page-map.json').write_text(json.dumps([dict(id=p['id'],page=NUMBER[p['id']],title=p['title'],quick=p['id'] in QUICK_IDS,sources=p['sources']) for p in PAGES],ensure_ascii=False,indent=2))
    (RUN/'commands.json').write_text(json.dumps(COMMANDS,ensure_ascii=False,indent=2))
    print(json.dumps({'pages':len(PAGES),'quick_pages':len(QUICK_IDS),'output':str(OUT)},ensure_ascii=False))

if __name__=='__main__':render()
