"""Строит локальный индекс: главы и текстовые файлы только из allowlist комплекта.

Индекс встраивается в HTML. При чтении не нужны сервер, сеть, исходные checkout
или чтение соседних пользовательских файлов. Секреты в комплект не добавляются.
"""
from html.parser import HTMLParser
import json
from pathlib import PurePosixPath
import re


class ChapterText(HTMLParser):
    """Собирает видимый текст, сохраняя ссылки на ближайший заголовок главы."""

    def __init__(self, identifier):
        super().__init__(convert_charrefs=True)
        self.identifier = identifier
        self.blocks = []
        self.current = None
        self.heading = None
        self.skip = 0

    def handle_starttag(self, tag, attrs):
        attrs = dict(attrs)
        if tag in {'script', 'style', 'button'}:
            self.skip += 1
        if self.skip:
            return
        if tag in {'h1', 'h2', 'h3'} and attrs.get('id'):
            self.current = {'href': '#' + attrs['id'], 'title': '', 'parts': []}
            self.blocks.append(self.current)
            self.heading = tag
        if self.current and tag in {'p', 'div', 'li', 'tr', 'td', 'th', 'br', 'pre', 'text', 'h4', 'dt', 'dd'}:
            self.current['parts'].append('\n')

    def handle_endtag(self, tag):
        if tag in {'script', 'style', 'button'}:
            self.skip = max(0, self.skip - 1)
        if self.skip:
            return
        if tag == self.heading:
            self.heading = None
        if self.current and tag in {'p', 'li', 'tr', 'td', 'th', 'text', 'h1', 'h2', 'h3', 'h4'}:
            self.current['parts'].append('\n')

    def handle_data(self, data):
        if self.skip or self.current is None:
            return
        self.current['parts'].append(data)
        if self.heading:
            self.current['title'] += data

    def entries(self, chapter_title):
        for block in self.blocks:
            text = re.sub(r'\s+', ' ', ''.join(block['parts'])).strip()
            yield {'kind': 'chapter', 'title': block['title'].strip(),
                   'chapter': chapter_title, 'path': 'index.html',
                   'href': block['href'], 'text': text}


def text_files(files):
    """Выбирает весь UTF-8-текст, кроме дубликатов HTML и служебных seed-копий.

    PNG/WAV/MP3 и другие бинарные данные не являются словарём. Подписи встроенных
    схем уже входят в главы. Manifest создаётся после индекса и проверяет его.
    """
    for name, data in sorted(files.items()):
        if name.startswith('tooling/inputs/') or name in {
            '.voice-textbook-bundle', 'BUNDLE-MANIFEST.json', 'tooling/chapters.html',
            'AUDIO-README.md', 'index.html', 'audio-index.html',
        } or PurePosixPath(name).suffix == '.html':
            continue
        try:
            text = data.decode('utf-8')
        except UnicodeDecodeError:
            continue
        if '\0' in text or not text.strip():
            continue
        yield name, text


def build_index(body, files):
    # Главы индексируются по подразделам; файл кода - целиком, чтобы поиск мог
    # сопоставить имя функции, параметры и комментарии из разных строк файла.
    entries = []
    chapter_count = 0
    for match in re.finditer(r'<section id="([^"]+)" class="chapter"[^>]*>(.*?)</section>', body, re.S):
        parser = ChapterText(match[1])
        parser.feed(match[2])
        chapter_count += 1
        chapter_title = parser.blocks[0]['title'].strip() if parser.blocks else match[1]
        entries.extend(parser.entries(chapter_title))
    file_count = 0
    for name, text in text_files(files):
        file_count += 1
        entries.append({'kind': 'file', 'title': PurePosixPath(name).name,
                        'path': name, 'href': name + '.html', 'text': text})
    return {'format': 1, 'chapters': chapter_count, 'files': file_count, 'entries': entries}


def inline_json(value):
    # Даже пример с закрывающим тегом script остаётся данными, не HTML-кодом.
    return json.dumps(value, ensure_ascii=False, separators=(',', ':')).replace(
        '&', '\\u0026').replace('<', '\\u003c').replace('>', '\\u003e').replace(
        '\u2028', '\\u2028').replace('\u2029', '\\u2029')
