# -*- coding: utf-8 -*-
"""原创离线合成样例；不连接模型、不读取账户。输出位于脚本旁。
已有输入保留，修改输入后可以重跑。失败边界由 --negative 单独演示。
"""
from pathlib import Path
import sys, json, csv, math, io, hashlib, html, base64, re
sys.stdout.reconfigure(encoding='utf-8')
sys.stderr.reconfigure(encoding='utf-8')
BASE = Path(__file__).resolve().parent
NEG = '--negative' in sys.argv
def write(name, value):
    p=BASE/name; p.parent.mkdir(parents=True,exist_ok=True)
    if isinstance(value,bytes): p.write_bytes(value)
    else: p.write_text(value,encoding='utf-8',newline='')
def seed(name,value):
    if not (BASE/name).exists(): write(name,value)
def jwrite(name,value): write(name,json.dumps(value,ensure_ascii=False,indent=2))
def jseed(name,value):
    if not (BASE/name).exists(): jwrite(name,value)
def load(name): return json.loads((BASE/name).read_text(encoding='utf-8'))
def table(name,heads,rows):
    f=io.StringIO(newline='');w=csv.writer(f);w.writerow(heads);w.writerows(rows);write(name,f.getvalue())
def page(name,title,body):
    write(name,'<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>'+html.escape(title)+'</title><style>body{max-width:1000px;margin:2rem auto;padding:0 1rem;font:17px/1.7 system-ui}table{border-collapse:collapse}td,th{border:1px solid #aaa;padding:.5rem}pre{white-space:pre-wrap;overflow-wrap:anywhere}img,svg{max-width:100%;height:auto}a{color:#125caa}</style><h1>'+html.escape(title)+'</h1>'+body+'</html>')
def emit(data): print(json.dumps(data,ensure_ascii=False))
from ebooklib import epub
from PIL import Image,ImageDraw
from urllib.parse import urlsplit,unquote

defaults={
    'chapter-1.md':'# Starting\n\nAn original synthetic guide.\n\nKeep the complete source when exporting a book.\n\n## Reading plan\n\nRead each paragraph and check the navigation.\n\n[Method chapter](chapter-2.md#chapter-2)\n\n[Method without fragment](chapter-2.md)\n',
    'chapter-2.md':'# Method\n\nRead inputs before interpreting results.\n\n## Original illustration\n\n![Original rectangular diagram](original-diagram.png)\n\n[Back to start](chapter-1.md#chapter-1)\n',
    'chapter-3.md':'# Limits\n\nPackaging does not establish factual accuracy.\n\nUnsupported Markdown must stop the export instead of silently disappearing.\n'
}
for name,text in defaults.items():seed(name,text)
if not (BASE/'original-diagram.png').exists():
    im=Image.new('RGB',(240,120),'#e9f3fd');ImageDraw.Draw(im).rectangle((30,30,210,90),fill='#3068a0');im.save(BASE/'original-diagram.png')

# 明确的安全子集：一章一个H1、H2/H3、普通多段、内部链接、本地PNG/JPEG/GIF。
# 不接受列表/表格/代码/强调/原始HTML，不尝试“猜测并忽略”未实现的语法。
inline_pattern=re.compile(r'(!?)\[([^\]\n]*)\]\(([^)\n]+)\)')
chapters_data=[];heading_ids={};resources={}
def plain_supported(text):
    if any(token in text for token in ['<','>','`','*','_','[',']','\\']):raise ValueError('不支持原始HTML或该Markdown内联语法，请改写为本例子集')
    return html.escape(text)
for number in range(1,4):
    name='missing-chapter.md' if NEG and number==2 else f'chapter-{number}.md'
    text=(BASE/name).read_text(encoding='utf-8');blocks=[];paragraph=[];ids=set();section=0;title=None
    def flush():
        if paragraph:blocks.append(('p',' '.join(paragraph),None));paragraph.clear()
    for line in text.splitlines():
        if not line.strip():flush();continue
        if line.expandtabs(4).startswith('    '):raise ValueError('不支持4空格或Tab缩进代码，请改成支持的普通段落')
        cells=line.strip().strip('|').split('|')
        if '|' in line and len(cells)>=2 and all(re.fullmatch(r'\s*:?-+:?\s*',cell) for cell in cells):raise ValueError('不支持Markdown表格分隔行，请改成普通段落')
        heading=re.fullmatch(r'(#{1,3}) (.+)',line)
        if heading:
            flush();level=len(heading[1]);label=heading[2];plain_supported(label)
            if title is None:
                if level!=1:raise ValueError('章节必须以一个H1标题开始')
                title=label;identifier=f'chapter-{number}'
            else:
                if level==1:raise ValueError('每章只支持一个H1标题')
                section+=1;identifier=f'chapter-{number}-section-{section}'
            ids.add(identifier);blocks.append((f'h{level}',label,identifier));continue
        if title is None:raise ValueError('章节必须以H1标题开始')
        if re.match(r'^\s*(?:#{1,6}\s|[-*+]\s|\d+[.)]\s|>|```|~~~|\|)',line) or re.fullmatch(r'\s*(?:={3,}|[-*_](?:\s*[-*_]){2,})\s*',line):raise ValueError('不支持该Markdown块结构，请改成段落/H2/H3')
        paragraph.append(line.strip())
    flush()
    if title is None:raise ValueError('章节为空或缺H1标题')
    chapter_name=f'chapter-{number}.md';heading_ids[chapter_name]=ids;chapters_data.append((chapter_name,title,blocks))

def local_image(target):
    url=urlsplit(target)
    if url.scheme or url.netloc or url.query or url.fragment:raise ValueError('图片只支持本地相对文件路径')
    name=unquote(url.path)
    if not name or '\\' in name or Path(name).is_absolute() or '..' in Path(name).parts:raise ValueError('图片相对路径不得逸出样例目录')
    path=(BASE/name).resolve()
    if not path.is_relative_to(BASE.resolve()) or not path.is_file():raise ValueError('本地图片缺失或路径逸出：'+target)
    with Image.open(path) as image:
        kind=image.format;image.verify()
    media={'PNG':'image/png','JPEG':'image/jpeg','GIF':'image/gif'}.get(kind)
    if not media:raise ValueError('本例只支持PNG/JPEG/GIF图片')
    name=Path(name).as_posix();resources[name]=(media,path.read_bytes());return name

def internal_link(target,current):
    url=urlsplit(target)
    if url.scheme or url.netloc or url.query or '\\' in target:raise ValueError('链接只支持本书章节和内部锚点')
    source=unquote(url.path) or current
    if source not in heading_ids:raise ValueError('内部链接章节不存在：'+target)
    fragment=unquote(url.fragment)
    if fragment and fragment not in heading_ids[source]:raise ValueError('内部链接锚点不存在：'+target)
    return ('' if not url.path else source.removesuffix('.md')+'.xhtml')+('#'+fragment if fragment else '')

def inline(text,current):
    out=[];start=0
    for match in inline_pattern.finditer(text):
        out.append(plain_supported(text[start:match.start()]));image,label,target=match.groups();safe_label=plain_supported(label)
        if image:out.append('<img src="'+html.escape(local_image(target),quote=True)+'" alt="'+html.escape(label,quote=True)+'"/>')
        else:out.append('<a href="'+html.escape(internal_link(target,current),quote=True)+'">'+safe_label+'</a>')
        start=match.end()
    out.append(plain_supported(text[start:]));return ''.join(out)

book=epub.EpubBook();book.set_identifier('urn:uuid:original-ai-guide-b2');book.set_title('Original offline analysis guide');book.set_language('en');book.add_author('Synthetic Example');chapters=[]
for name,title,blocks in chapters_data:
    body=[]
    for tag,text,identifier in blocks:
        body.append('<'+tag+(' id="'+identifier+'"' if identifier else '')+'>'+inline(text,name)+'</'+tag+'>')
    chapter=epub.EpubHtml(title=title,file_name=name.removesuffix('.md')+'.xhtml',lang='en');chapter.content=''.join(body);book.add_item(chapter);chapters.append(chapter)
for index,(name,(media,content)) in enumerate(sorted(resources.items())):book.add_item(epub.EpubItem(uid=f'figure-{index}',file_name=name,media_type=media,content=content))
book.toc=chapters;book.spine=['nav']+chapters;book.add_item(epub.EpubNcx());book.add_item(epub.EpubNav());epub.write_epub(str(BASE/'guide.epub'),book)
read=epub.read_epub(str(BASE/'guide.epub'));import zipfile
preview=''.join(c.content for c in chapters)
for i in range(1,4):preview=preview.replace(f'chapter-{i}.xhtml#','#').replace(f'href="chapter-{i}.xhtml"',f'href="#chapter-{i}"')
page('epub-preview.html','EPUB 章节预览','<p>仅支持H1/H2/H3、多段、内部链接和本地PNG/JPEG/GIF；原始HTML、列表、表格、代码与强调会拒绝。此页用于检查，最终阅读器分页仍需人工核对。</p><ol>'+''.join(f'<li><a href="#chapter-{i}">Chapter {i}</a></li>' for i in range(1,4))+'</ol>'+preview)
with zipfile.ZipFile(BASE/'guide.epub') as z:jwrite('package-list.json',z.namelist())
emit({'spine':[x[0] for x in read.spine],'chapters':len(chapters),'navigation':len(read.toc),'source_heading_ids':{k:sorted(v) for k,v in heading_ids.items()},'images':sorted(resources)})
