#!/usr/bin/env python3
"""
Rebuild RAG index with improved chunking:
- Remove file headers and page markers
- Strict chunk size enforcement
- Better paragraph-aware splitting
- Filter out empty/noise chunks
"""
import json
import re
import math
from pathlib import Path
from collections import Counter

from config import cfg
PROCESSED_DIR = cfg.processed_dir
INDEX_FILE = cfg.index_file
KEYWORD_FILE = cfg.keyword_file

CHUNK_SIZE = 512
CHUNK_OVERLAP = 64
MIN_CHUNK_LEN = 30  # Discard chunks shorter than this


def clean_text(text, filename):
    """Remove noise from extracted PDF text and merge broken lines"""
    # Remove file header line
    text = re.sub(r'^# 源文件:.*\n*', '', text)
    # Remove page markers
    text = re.sub(r'--- 第\d+页 ---\n?', '', text)
    # Remove leading/trailing whitespace per line
    lines = [line.strip() for line in text.split('\n')]

    # Merge broken lines from PDF extraction.
    # PDF text has line breaks from page layout, not real paragraph breaks.
    # Strategy: only treat as paragraph break if previous text ends with
    # sentence-ending punctuation AND there's a blank line.
    merged = []
    buf = ''
    blank_count = 0
    for line in lines:
        if not line:
            blank_count += 1
            continue
        if buf:
            last_char = buf[-1] if buf else ''
            ends_sentence = last_char in '。！？；：）》」』"'
            # Real paragraph break = ended with punctuation + had blank line(s)
            if ends_sentence and blank_count > 0:
                merged.append(buf)
                merged.append('')  # preserve paragraph separator
                buf = line
            else:
                # Merge: either no blank line, or previous didn't end sentence
                buf += line
        else:
            buf = line
        blank_count = 0
    if buf:
        merged.append(buf)

    text = '\n'.join(merged)
    # Collapse multiple blank lines
    text = re.sub(r'\n{3,}', '\n\n', text)
    return text.strip()


def _smart_split(text, max_len, overlap):
    """Split long text at natural break points (punctuation), not mid-word"""
    # Priority: sentence-end > clause-end > comma > any punct > force
    break_chars = '。！？；\n'
    soft_break_chars = '，、：）》」』"'

    pieces = []
    start = 0
    while start < len(text):
        if start + max_len >= len(text):
            pieces.append(text[start:])
            break

        end = start + max_len
        # Look backwards for best break point
        best = -1
        # First try sentence-ending punctuation
        for i in range(end, max(start + max_len // 2, start), -1):
            if i < len(text) and text[i] in break_chars:
                best = i + 1
                break
        # If not found, try soft breaks (comma etc)
        if best == -1:
            for i in range(end, max(start + max_len // 2, start), -1):
                if i < len(text) and text[i] in soft_break_chars:
                    best = i + 1
                    break
        # Last resort: hard cut
        if best == -1:
            best = end

        pieces.append(text[start:best])
        # Overlap: go back a bit
        start = max(best - overlap, start + 1)
        if start >= len(text):
            break

    return pieces


def split_chunks(text, source_file):
    """Split text into chunks with strict size enforcement.
    Tables marked with [TABLE_START]...[TABLE_END] are kept as whole chunks."""
    chunks = []
    chunk_id = 0

    # First: extract tables as separate blocks
    # Split text into table blocks and text blocks
    parts = re.split(r'(\[TABLE_START\].*?\[TABLE_END\])', text, flags=re.DOTALL)

    all_paragraphs = []
    for part in parts:
        part = part.strip()
        if not part:
            continue
        if part.startswith('[TABLE_START]'):
            # Keep table as a single "paragraph" (won't be split further)
            table_content = part.replace('[TABLE_START]', '').replace('[TABLE_END]', '').strip()
            if table_content:
                all_paragraphs.append(('table', table_content))
        else:
            # Regular text: split into paragraphs
            for para in re.split(r'\n\s*\n', part):
                para = para.strip()
                if para and len(para) >= 5:
                    all_paragraphs.append(('text', para))

    # Now process paragraphs
    paragraphs_iter = all_paragraphs

    current_chunk = ''

    for ptype, para in paragraphs_iter:
        # Tables: always emit as their own chunk(s)
        if ptype == 'table':
            # Flush current text chunk first
            if current_chunk and len(current_chunk) >= MIN_CHUNK_LEN:
                chunks.append({
                    'id': f'{source_file}#chunk{chunk_id}',
                    'source': source_file,
                    'chunk_id': chunk_id,
                    'text': current_chunk,
                    'length': len(current_chunk),
                })
                chunk_id += 1
                current_chunk = ''

            # If table fits in one chunk, keep whole; otherwise split by rows
            if len(para) <= CHUNK_SIZE * 2:  # Allow tables up to 2x chunk size
                chunks.append({
                    'id': f'{source_file}#chunk{chunk_id}',
                    'source': source_file,
                    'chunk_id': chunk_id,
                    'text': para,
                    'length': len(para),
                })
                chunk_id += 1
            else:
                # Large table: split keeping header + groups of rows
                lines = para.split('\n')
                header = '\n'.join(lines[:2]) if len(lines) > 2 else ''  # header + separator
                table_chunk = header
                for line in lines[2:]:
                    if len(table_chunk) + len(line) + 1 > CHUNK_SIZE * 2:
                        if table_chunk:
                            chunks.append({
                                'id': f'{source_file}#chunk{chunk_id}',
                                'source': source_file,
                                'chunk_id': chunk_id,
                                'text': table_chunk,
                                'length': len(table_chunk),
                            })
                            chunk_id += 1
                        table_chunk = header + '\n' + line  # restart with header
                    else:
                        table_chunk += '\n' + line
                if table_chunk and len(table_chunk) >= MIN_CHUNK_LEN:
                    chunks.append({
                        'id': f'{source_file}#chunk{chunk_id}',
                        'source': source_file,
                        'chunk_id': chunk_id,
                        'text': table_chunk,
                        'length': len(table_chunk),
                    })
                    chunk_id += 1
            continue

        # Regular text paragraph
        # If adding this paragraph fits in chunk_size
        if len(current_chunk) + len(para) + 1 <= CHUNK_SIZE:
            current_chunk = (current_chunk + '\n' + para).strip() if current_chunk else para
        else:
            # Save current chunk if it has content
            if current_chunk and len(current_chunk) >= MIN_CHUNK_LEN:
                chunks.append({
                    'id': f'{source_file}#chunk{chunk_id}',
                    'source': source_file,
                    'chunk_id': chunk_id,
                    'text': current_chunk,
                    'length': len(current_chunk),
                })
                chunk_id += 1

            # If paragraph itself is too long, split by sentences/characters
            if len(para) > CHUNK_SIZE:
                # Try sentence splitting first (at major punctuation)
                sentences = re.split(r'(?<=[。！？；\n])', para)
                # If no split happened (single long sentence), try comma-level
                if len(sentences) <= 1 and len(para) > CHUNK_SIZE:
                    sentences = _smart_split(para, CHUNK_SIZE, CHUNK_OVERLAP)
                    # Already split into right-sized pieces, emit directly
                    for piece in sentences:
                        if len(piece.strip()) >= MIN_CHUNK_LEN:
                            chunks.append({
                                'id': f'{source_file}#chunk{chunk_id}',
                                'source': source_file,
                                'chunk_id': chunk_id,
                                'text': piece.strip(),
                                'length': len(piece.strip()),
                            })
                            chunk_id += 1
                    current_chunk = ''
                    continue
                sub_chunk = ''
                for sent in sentences:
                    sent = sent.strip()
                    if not sent:
                        continue
                    if len(sub_chunk) + len(sent) + 1 <= CHUNK_SIZE:
                        sub_chunk = (sub_chunk + sent).strip() if sub_chunk else sent
                    else:
                        if sub_chunk and len(sub_chunk) >= MIN_CHUNK_LEN:
                            chunks.append({
                                'id': f'{source_file}#chunk{chunk_id}',
                                'source': source_file,
                                'chunk_id': chunk_id,
                                'text': sub_chunk,
                                'length': len(sub_chunk),
                            })
                            chunk_id += 1
                        # If single sentence > CHUNK_SIZE, smart split at punctuation
                        if len(sent) > CHUNK_SIZE:
                            pieces = _smart_split(sent, CHUNK_SIZE, CHUNK_OVERLAP)
                            for piece in pieces:
                                if len(piece) >= MIN_CHUNK_LEN:
                                    chunks.append({
                                        'id': f'{source_file}#chunk{chunk_id}',
                                        'source': source_file,
                                        'chunk_id': chunk_id,
                                        'text': piece,
                                        'length': len(piece),
                                    })
                                    chunk_id += 1
                            sub_chunk = ''
                        else:
                            sub_chunk = sent
                # Don't forget remaining sub_chunk
                if sub_chunk and len(sub_chunk) >= MIN_CHUNK_LEN:
                    current_chunk = sub_chunk
                else:
                    current_chunk = ''
            else:
                # Start new chunk with overlap from previous
                overlap = current_chunk[-CHUNK_OVERLAP:] if len(current_chunk) > CHUNK_OVERLAP else ''
                current_chunk = (overlap + '\n' + para).strip() if overlap else para

    # Last chunk
    if current_chunk and len(current_chunk) >= MIN_CHUNK_LEN:
        chunks.append({
            'id': f'{source_file}#chunk{chunk_id}',
            'source': source_file,
            'chunk_id': chunk_id,
            'text': current_chunk,
            'length': len(current_chunk),
        })

    return chunks


# Build index
all_chunks = []
txt_files = sorted(PROCESSED_DIR.glob('*.txt'))
print(f'Processing {len(txt_files)} text files...')

for i, txt_path in enumerate(txt_files):
    text = txt_path.read_text(encoding='utf-8')
    text = clean_text(text, txt_path.stem)

    if len(text) < 30:
        continue

    chunks = split_chunks(text, txt_path.stem)
    all_chunks.extend(chunks)

    if (i + 1) % 200 == 0:
        print(f'  Progress: {i+1}/{len(txt_files)}, chunks: {len(all_chunks)}')

# Build keyword inverted index
print(f'Building keyword index... ({len(all_chunks)} chunks)')

keyword_index = {}
for chunk in all_chunks:
    words = set()
    text = chunk['text'].lower()
    # Chinese bigram + trigram
    chinese_chars = re.findall(r'[\u4e00-\u9fff]+', text)
    for cc in chinese_chars:
        for j in range(len(cc) - 1):
            words.add(cc[j:j+2])
        if len(cc) >= 3:
            for j in range(len(cc) - 2):
                words.add(cc[j:j+3])
    # English words
    eng_words = re.findall(r'[a-z]+', text)
    words.update(eng_words)

    for w in words:
        if w not in keyword_index:
            keyword_index[w] = []
        keyword_index[w].append(chunk['id'])

# Save
index_data = {
    'total_chunks': len(all_chunks),
    'total_files': len(txt_files),
    'chunk_size': CHUNK_SIZE,
    'chunks': all_chunks,
    'keyword_index_size': len(keyword_index),
}

with open(INDEX_FILE, 'w', encoding='utf-8') as f:
    json.dump(index_data, f, ensure_ascii=False)

with open(KEYWORD_FILE, 'w', encoding='utf-8') as f:
    json.dump(keyword_index, f, ensure_ascii=False)

# Stats
lengths = [c['length'] for c in all_chunks]
print(f'\nDone!')
print(f'  Total chunks: {len(all_chunks)}')
print(f'  Keywords: {len(keyword_index)}')
print(f'  Avg length: {sum(lengths)/len(lengths):.0f}')
print(f'  Min/Max: {min(lengths)}/{max(lengths)}')
print(f'  <100: {sum(1 for l in lengths if l < 100)} ({sum(1 for l in lengths if l < 100)*100/len(lengths):.1f}%)')
print(f'  >1000: {sum(1 for l in lengths if l > 1000)} ({sum(1 for l in lengths if l > 1000)*100/len(lengths):.1f}%)')
print(f'  Index size: {INDEX_FILE.stat().st_size/1024/1024:.1f} MB')
