"""JSON to Markdown converter with streaming support."""
import re
from json.decoder import scanstring

__version__ = "0.1.0"

# === Streaming tokenizer ===
TOKEN_RE = re.compile(r'''
    (")
    |(-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?)
    |(true|false|null)
    |([{}\[\]:,])
    |(\s+)
''', re.VERBOSE)

def tokenize_stream(chunks):
    """Yield tokens from an iterator of chunks."""
    buf = ""
    for chunk in chunks:
        buf += chunk
        i = 0
        while i < len(buf):
            remaining = len(buf) - i
            if remaining < 32 and chunk != "":
                break
            m = TOKEN_RE.match(buf, i)
            if not m:
                raise ValueError(f"Unexpected at {i}: {buf[i:i+20]!r}")
            if m.group(5):
                i = m.end()
            elif m.group(1):
                try:
                    val, end = scanstring(buf, i + 1)
                    yield ('STR', val)
                    i = end
                except ValueError:
                    break
            elif m.group(2):
                end = m.end()
                if end >= len(buf) and chunk != "":
                    break
                yield ('NUM', m.group(2))
                i = end
            elif m.group(3):
                end = m.end()
                if end >= len(buf) and chunk != "":
                    break
                yield ('LIT', m.group(3))
                i = end
            elif m.group(4):
                yield ('SYM', m.group(4))
                i = m.end()
        buf = buf[i:]
    
    # Process remaining buffer
    i = 0
    while i < len(buf):
        m = TOKEN_RE.match(buf, i)
        if not m:
            if buf[i:].strip():
                raise ValueError(f"Unexpected at end: {buf[i:]!r}")
            break
        if m.group(5):
            i = m.end()
        elif m.group(1):
            val, end = scanstring(buf, i + 1)
            yield ('STR', val)
            i = end
        elif m.group(2):
            yield ('NUM', m.group(2))
            i = m.end()
        elif m.group(3):
            yield ('LIT', m.group(3))
            i = m.end()
        elif m.group(4):
            yield ('SYM', m.group(4))
            i = m.end()

def chunk_reader(f, size=8192):
    """Read file in chunks."""
    while True:
        chunk = f.read(size)
        if not chunk:
            yield ""
            break
        yield chunk

# === Helpers ===
def is_safe_value(s):
    if not s or "\n" in s: return False
    if s in ('null', 'true', 'false', '[]', '{}', '""'): return False
    try: float(s); return False
    except: pass
    return not (s[0].isspace() or s[-1].isspace() or any(c in s for c in ':[]{}"\''))

def emit(depth, content):
    return "> " * depth + content + "  "

# === Streaming parser ===
def parse_stream(tokens):
    """Yield markdown lines from token stream."""
    tokens = iter(tokens)
    current = [None]
    
    def peek():
        if current[0] is None:
            current[0] = next(tokens, (None, None))
        return current[0]
    
    def consume():
        t = peek()
        current[0] = None
        return t
    
    def expect(sym):
        t = consume()
        assert t == ('SYM', sym), f"expected {sym}, got {t}"
    
    def parse_value(depth):
        t, v = peek()
        if t == 'SYM' and v == '{':
            consume()
            nt, nv = peek()
            if nt == 'SYM' and nv == '}':
                consume()
                yield emit(depth, "{}")
            else:
                yield from parse_object_inner(depth)
        elif t == 'SYM' and v == '[':
            consume()
            nt, nv = peek()
            if nt == 'SYM' and nv == ']':
                consume()
                yield emit(depth, "[]")
            else:
                yield from parse_array_inner(depth)
        elif t == 'STR':
            consume()
            if v == "":
                yield emit(depth, '""')
            elif is_safe_value(v):
                yield emit(depth, v)
            else:
                for line in v.split('\n'):
                    yield emit(depth, line)
        elif t in ('NUM', 'LIT'):
            consume()
            yield emit(depth, v)
        elif t == (None, None) or t is None:
            return
        else:
            raise ValueError(f"Unexpected token: {t} {v}")
    
    def parse_object_inner(depth):
        t, v = peek()
        while not (t == 'SYM' and v == '}'):
            kt, key = consume()
            assert kt == 'STR', f"expected string key, got {kt}"
            expect(':')
            
            key_repr = key if key.isidentifier() else f"<q>{key}</q>"
            vt, vv = peek()
            
            if vt == 'SYM' and vv == '{':
                consume()
                nt, nv = peek()
                if nt == 'SYM' and nv == '}':
                    consume()
                    yield emit(depth, f"{key_repr}: {{}}")
                else:
                    yield emit(depth, f"# {key_repr}:")
                    yield from parse_object_inner(depth + 1)
            elif vt == 'SYM' and vv == '[':
                consume()
                nt, nv = peek()
                if nt == 'SYM' and nv == ']':
                    consume()
                    yield emit(depth, f"{key_repr}: []")
                else:
                    yield emit(depth, f"# {key_repr}:")
                    yield from parse_array_inner(depth + 1)
            elif vt == 'STR' and vv == "":
                consume()
                yield emit(depth, f'{key_repr}: ""')
            elif vt == 'STR' and is_safe_value(vv):
                consume()
                yield emit(depth, f"{key_repr}: {vv}")
            elif vt in ('NUM', 'LIT'):
                consume()
                yield emit(depth, f"{key_repr}: {vv}")
            else:
                yield emit(depth, f"{key_repr}:")
                yield from parse_value(depth + 1)
            
            t, v = peek()
            if t == 'SYM' and v == ',':
                consume()
                t, v = peek()
        expect('}')
    
    def parse_array_inner(depth):
        first = True
        t, v = peek()
        while not (t == 'SYM' and v == ']'):
            if not first:
                yield ">  "
            first = False
            
            if t == 'SYM' and v == '{':
                consume()
                nt, nv = peek()
                if nt == 'SYM' and nv == '}':
                    consume()
                    yield emit(depth, "{}")
                else:
                    yield from parse_object_inner(depth + 1)
            elif t == 'SYM' and v == '[':
                consume()
                nt, nv = peek()
                if nt == 'SYM' and nv == ']':
                    consume()
                    yield emit(depth, "[]")
                else:
                    yield from parse_array_inner(depth + 1)
            elif t == 'STR':
                consume()
                if v == "":
                    yield emit(depth, '""')
                elif is_safe_value(v):
                    yield emit(depth, v)
                else:
                    for line in v.split('\n'):
                        yield emit(depth + 1, line)
            elif t in ('NUM', 'LIT'):
                consume()
                yield emit(depth, v)
            else:
                raise ValueError(f"Unexpected in array: {t} {v}")
            
            t, v = peek()
            if t == 'SYM' and v == ',':
                consume()
                t, v = peek()
        expect(']')
    
    while True:
        t, v = peek()
        if t is None:
            break
        yield from parse_value(1)
        yield ""

# === Public API ===
def stream_json_to_md(f):
    """Stream JSON from file handle to markdown lines."""
    tokens = tokenize_stream(chunk_reader(f))
    yield from parse_stream(tokens)

def to_markdown_from_str(s):
    """Convert JSON string to markdown string."""
    tokens = tokenize_stream(iter([s, ""]))
    lines = list(parse_stream(tokens))
    while lines and lines[-1] == "":
        lines.pop()
    return "\n".join(lines)

def to_markdown(obj):
    """Convert Python object to markdown string."""
    import json
    return to_markdown_from_str(json.dumps(obj))
