"""
PyWasot - Python bindings for the Wasot markup language.

Usage:
    import pywasot

    for event in pywasot.parse(".section\\nHello world"):
        match event:
            case pywasot.BlockStarted():
                print(f"Block: {event.type_name}")
            case pywasot.Text():
                print(f"Text: {event.content}")
"""
from __future__ import annotations

from abc import ABC
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Iterator, Optional, Union

# Import raw Rust types (internal use)
from ._pywasot import (
    # Core data types from Rust
    TargetType,
    ExternalIdReference,
    # Raw events from Rust
    BlockStartedEvent as _RawBlockStarted,
    BlockFinishedEvent as _RawBlockFinished,
    TextEvent as _RawText,
    ContextLineEvent as _RawContextLine,
    VoidElementEvent as _RawVoidElement,
    # Iterators and utilities
    WasotIterator,
    Specificity,
    # Functions
    parse_wasot_string as _parse_wasot_string,
    parse_wasot_file as _parse_wasot_file,
    calculate_specificity,
    parse_metaline,
)


# =============================================================================
# Internal Data Holders (for creating objects without Rust)
# =============================================================================

@dataclass
class _QuotedStringData:
    content: str
    quoted: bool = False
    links: list = field(default_factory=list)


@dataclass
class _LinkData:
    target: Optional[str] = None
    target_type: Any = None
    name: Optional[_QuotedStringData] = None
    arguments: Optional['_ArgumentsData'] = None
    start_index: int = 0
    length: int = 0
    document_ref: Any = None


@dataclass
class _FootnoteData:
    marker: int
    link: str


@dataclass
class _ArgumentsData:
    args: list = field(default_factory=list)
    kwargs: list = field(default_factory=list)


@dataclass
class _MetaLineData:
    type_name: str
    level: int = 1
    inline_text: _QuotedStringData = field(default_factory=lambda: _QuotedStringData(''))
    line_index: Optional[int] = None
    arguments: _ArgumentsData = field(default_factory=_ArgumentsData)
    block_id: Optional[str] = None
    is_void: bool = False


@dataclass
class _BlockStartedData:
    meta_line: _MetaLineData
    context: Any = None


@dataclass
class _TextData:
    lines: list = field(default_factory=list)
    links: list = field(default_factory=list)
    footnotes: list = field(default_factory=list)


@dataclass
class _ContextLineData:
    meta_line: _MetaLineData


@dataclass
class _VoidElementData:
    meta_line: _MetaLineData


# =============================================================================
# DX Wrapper Classes
# =============================================================================

class QuotedString:
    """A string value that may have been quoted in source."""
    __slots__ = ('_raw',)

    def __init__(self, raw):
        self._raw = raw

    @classmethod
    def create(cls, content: str, *, quoted: bool = False) -> 'QuotedString':
        """Create a QuotedString directly."""
        return cls(_QuotedStringData(content=content, quoted=quoted, links=[]))

    def clone(self, *, content: str = None, quoted: bool = None) -> 'QuotedString':
        """Create a copy with optional field overrides."""
        return QuotedString(_QuotedStringData(
            content=content if content is not None else self.content,
            quoted=quoted if quoted is not None else self.quoted,
            links=list(self._raw.links),
        ))

    @property
    def content(self) -> str:
        return self._raw.content

    @property
    def quoted(self) -> bool:
        return self._raw.quoted

    @property
    def links(self) -> list['Link']:
        return [Link(l) for l in self._raw.links]

    def __str__(self) -> str:
        return self.content

    def __repr__(self) -> str:
        if self.quoted:
            return f'QuotedString({self.content!r}, quoted=True)'
        return f'QuotedString({self.content!r})'

    def __eq__(self, other) -> bool:
        if isinstance(other, str):
            return self.content == other
        if isinstance(other, QuotedString):
            return self.content == other.content
        return False

    def __hash__(self) -> int:
        return hash(self.content)


class Link:
    """A link within text content."""
    __slots__ = ('_raw',)

    def __init__(self, raw):
        self._raw = raw

    @classmethod
    def create(cls, target: str, *, name: Optional[str] = None,
               target_type: TargetType = None, start_index: int = 0, length: int = 0) -> 'Link':
        """Create a Link directly."""
        name_data = _QuotedStringData(content=name) if name else None
        if target_type is None:
            target_type = TargetType.URI
        return cls(_LinkData(target=target, target_type=target_type, name=name_data,
                             arguments=_ArgumentsData(), start_index=start_index, length=length))

    def clone(self, *, name: Optional[str] = None, target: str = None,
              target_type: TargetType = None, start_index: int = None, length: int = None) -> 'Link':
        """Create a copy with optional field overrides."""
        name_data = None
        if name is not None:
            name_data = _QuotedStringData(content=name)
        elif self._raw.name:
            name_data = _QuotedStringData(content=self._raw.name.content, quoted=self._raw.name.quoted)
        return Link(_LinkData(
            target=target if target is not None else self._raw.target,
            target_type=target_type if target_type is not None else self._raw.target_type,
            name=name_data,
            arguments=_ArgumentsData(args=list(self._raw.arguments.args), kwargs=list(self._raw.arguments.kwargs)) if self._raw.arguments else _ArgumentsData(),
            start_index=start_index if start_index is not None else self._raw.start_index,
            length=length if length is not None else self._raw.length,
        ))

    @property
    def name(self) -> Optional[QuotedString]:
        return QuotedString(self._raw.name) if self._raw.name else None

    @property
    def name_str(self) -> Optional[str]:
        return self._raw.name.content if self._raw.name else None

    @property
    def target(self) -> Optional[str]:
        return self._raw.target

    @property
    def target_type(self) -> TargetType:
        return self._raw.target_type

    @property
    def is_uri(self) -> bool:
        return self._raw.target_type == TargetType.URI

    @property
    def is_id(self) -> bool:
        return self._raw.target_type == TargetType.ID

    @property
    def is_footnote(self) -> bool:
        return self._raw.target_type == TargetType.FOOTNOTE

    @property
    def start_index(self) -> int:
        return self._raw.start_index

    @property
    def length(self) -> int:
        return self._raw.length

    @property
    def arguments(self) -> 'Arguments':
        return Arguments(self._raw.arguments)

    def __repr__(self) -> str:
        name = f'{self.name_str!r}' if self.name_str else 'None'
        return f'Link({name} -> {self.target!r})'


class Footnote:
    """A footnote definition."""
    __slots__ = ('_raw',)

    def __init__(self, raw):
        self._raw = raw

    @classmethod
    def create(cls, marker: int, link: str) -> 'Footnote':
        """Create a Footnote directly."""
        return cls(_FootnoteData(marker=marker, link=link))

    def clone(self, *, marker: int = None, link: str = None) -> 'Footnote':
        """Create a copy with optional field overrides."""
        return Footnote(_FootnoteData(
            marker=marker if marker is not None else self._raw.marker,
            link=link if link is not None else self._raw.link,
        ))

    @property
    def marker(self) -> int:
        return self._raw.marker

    @property
    def link(self) -> str:
        return self._raw.link

    def __repr__(self) -> str:
        return f'Footnote(*{self.marker}: {self.link!r})'


class Arguments:
    """Parsed arguments from a block. Supports dict-style access for kwargs."""
    __slots__ = ('_raw',)

    def __init__(self, raw):
        self._raw = raw

    @classmethod
    def create(cls, args: Optional[list[str]] = None,
               kwargs: Optional[dict[str, str]] = None) -> 'Arguments':
        """Create Arguments directly."""
        args_data = [_QuotedStringData(content=a) for a in (args or [])]
        kwargs_data = [(_QuotedStringData(content=k), _QuotedStringData(content=v))
                       for k, v in (kwargs or {}).items()]
        return cls(_ArgumentsData(args=args_data, kwargs=kwargs_data))

    def clone(self, *, args: list[str] = None, kwargs: dict[str, str] = None) -> 'Arguments':
        """Create a copy with optional field overrides."""
        args_data = [_QuotedStringData(content=a) for a in (args if args is not None else self.args_str)]
        kwargs_data = [(_QuotedStringData(content=k), _QuotedStringData(content=v))
                       for k, v in (kwargs if kwargs is not None else self.kwargs_dict).items()]
        return Arguments(_ArgumentsData(args=args_data, kwargs=kwargs_data))

    @property
    def args(self) -> list[QuotedString]:
        return [QuotedString(a) for a in self._raw.args]

    @property
    def args_str(self) -> list[str]:
        return [a.content for a in self._raw.args]

    @property
    def kwargs(self) -> list[tuple[QuotedString, QuotedString]]:
        return [(QuotedString(k), QuotedString(v)) for k, v in self._raw.kwargs]

    @property
    def kwargs_dict(self) -> dict[str, str]:
        return {k.content: v.content for k, v in self._raw.kwargs}

    def get(self, key: str, default: str = None) -> Optional[str]:
        for k, v in self._raw.kwargs:
            if k.content == key:
                return v.content
        return default

    def __getitem__(self, key: str) -> str:
        for k, v in self._raw.kwargs:
            if k.content == key:
                return v.content
        raise KeyError(key)

    def __contains__(self, key: str) -> bool:
        return any(k.content == key for k, _ in self._raw.kwargs)

    def __len__(self) -> int:
        return len(self._raw.args) + len(self._raw.kwargs)

    def __bool__(self) -> bool:
        return len(self) > 0

    def __repr__(self) -> str:
        args = ', '.join(self.args_str)
        kwargs = ', '.join(f'{k}={v!r}' for k, v in self.kwargs_dict.items())
        parts = [p for p in [args, kwargs] if p]
        return f'Arguments({", ".join(parts)})'


class MetaLine:
    """A parsed metaline representing a Wasot block definition."""
    __slots__ = ('_raw',)

    def __init__(self, raw):
        self._raw = raw

    @classmethod
    def create(cls, type_name: str, *, level: int = 1, text: str = '',
               line_index: Optional[int] = None, args: Optional[list[str]] = None,
               kwargs: Optional[dict[str, str]] = None, id: Optional[str] = None,
               is_void: bool = False) -> 'MetaLine':
        """Create a MetaLine directly."""
        args_data = [_QuotedStringData(content=a) for a in (args or [])]
        kwargs_data = [(_QuotedStringData(content=k), _QuotedStringData(content=v))
                       for k, v in (kwargs or {}).items()]
        return cls(_MetaLineData(type_name=type_name, level=level,
                                 inline_text=_QuotedStringData(content=text),
                                 line_index=line_index,
                                 arguments=_ArgumentsData(args=args_data, kwargs=kwargs_data),
                                 block_id=id, is_void=is_void))

    def clone(self, *, type_name: str = None, level: int = None, text: str = None,
              line_index: int = None, args: list[str] = None, kwargs: dict[str, str] = None,
              id: str = None, is_void: bool = None) -> 'MetaLine':
        """Create a copy with optional field overrides."""
        args_data = [_QuotedStringData(content=a) for a in (args if args is not None else self.args)]
        kwargs_data = [(_QuotedStringData(content=k), _QuotedStringData(content=v))
                       for k, v in (kwargs if kwargs is not None else self.kwargs).items()]
        return MetaLine(_MetaLineData(
            type_name=type_name if type_name is not None else self.type_name,
            level=level if level is not None else self.level,
            inline_text=_QuotedStringData(content=text if text is not None else self.text),
            line_index=line_index if line_index is not None else self.line_index,
            arguments=_ArgumentsData(args=args_data, kwargs=kwargs_data),
            block_id=id if id is not None else self.block_id,
            is_void=is_void if is_void is not None else self.is_void,
        ))

    @property
    def type_name(self) -> str:
        return self._raw.type_name

    @property
    def type(self) -> str:
        return self._raw.type_name

    @property
    def level(self) -> int:
        return self._raw.level

    @property
    def inline_text(self) -> QuotedString:
        return QuotedString(self._raw.inline_text)

    @property
    def text(self) -> str:
        return self._raw.inline_text.content

    @property
    def line_index(self) -> Optional[int]:
        return self._raw.line_index

    @property
    def arguments(self) -> Arguments:
        return Arguments(self._raw.arguments)

    @property
    def args(self) -> list[str]:
        return [a.content for a in self._raw.arguments.args]

    @property
    def kwargs(self) -> dict[str, str]:
        return {k.content: v.content for k, v in self._raw.arguments.kwargs}

    @property
    def block_id(self) -> Optional[str]:
        return self._raw.block_id

    @property
    def id(self) -> Optional[str]:
        return self._raw.block_id

    @property
    def is_void(self) -> bool:
        return self._raw.is_void

    def __repr__(self) -> str:
        parts = [f'.{"." * (self.level - 1)}{self.type_name}']
        if self.block_id:
            parts.append(f':{self.block_id}')
        if self.text:
            parts.append(f'.. {self.text}')
        return f'MetaLine({" ".join(parts)})'


class Context:
    """Context object for storing values during parsing."""
    __slots__ = ('_raw',)

    def __init__(self, raw):
        self._raw = raw

    def put(self, key: str, value) -> None:
        self._raw.put(key, value)

    def get(self, key: str, default=None):
        val = self._raw.get(key)
        return val if val is not None else default

    def put_scoped(self, key: str, value) -> None:
        self._raw.put_scoped(key, value)

    def get_scoped(self, key: str, default=None):
        val = self._raw.get_scoped(key)
        return val if val is not None else default

    @property
    def items(self) -> dict:
        return self._raw.items

    @property
    def scoped_items(self) -> dict:
        return self._raw.scoped_items

    def __getitem__(self, key: str):
        return self.get(key)

    def __setitem__(self, key: str, value):
        self.put(key, value)

    def __contains__(self, key: str) -> bool:
        return self.get(key) is not None

    def __repr__(self) -> str:
        return f'Context({dict(self.items)})'


# =============================================================================
# Event Classes
# =============================================================================

class BlockStarted:
    """Event emitted when a new block starts."""
    __slots__ = ('_raw',)

    def __init__(self, raw):
        self._raw = raw

    @classmethod
    def create(cls, type_name: str, *, level: int = 1, text: str = '',
               line_index: Optional[int] = None, args: Optional[list[str]] = None,
               kwargs: Optional[dict[str, str]] = None, id: Optional[str] = None) -> 'BlockStarted':
        """Create a BlockStarted event directly."""
        meta = MetaLine.create(type_name, level=level, text=text, line_index=line_index,
                               args=args, kwargs=kwargs, id=id)
        return cls(_BlockStartedData(meta_line=meta._raw, context=None))

    def clone(self, *, type_name: str = None, level: int = None, text: str = None,
              line_index: int = None, args: list[str] = None, kwargs: dict[str, str] = None,
              id: str = None) -> 'BlockStarted':
        """Create a copy with optional field overrides."""
        meta = self.meta.clone(
            type_name=type_name,
            level=level,
            text=text,
            line_index=line_index,
            args=args,
            kwargs=kwargs,
            id=id,
        )
        return BlockStarted(_BlockStartedData(meta_line=meta._raw, context=None))

    @property
    def meta(self) -> MetaLine:
        return MetaLine(self._raw.meta_line)

    @property
    def meta_line(self) -> MetaLine:
        return self.meta

    @property
    def context(self) -> Optional[Context]:
        return Context(self._raw.context) if self._raw.context else None

    @property
    def type_name(self) -> str:
        return self._raw.meta_line.type_name

    @property
    def type(self) -> str:
        return self._raw.meta_line.type_name

    @property
    def level(self) -> int:
        return self._raw.meta_line.level

    @property
    def text(self) -> str:
        return self._raw.meta_line.inline_text.content

    @property
    def inline_text(self) -> str:
        return self._raw.meta_line.inline_text.content

    @property
    def id(self) -> Optional[str]:
        return self._raw.meta_line.block_id

    @property
    def block_id(self) -> Optional[str]:
        return self._raw.meta_line.block_id

    @property
    def args(self) -> list[str]:
        return [a.content for a in self._raw.meta_line.arguments.args]

    @property
    def kwargs(self) -> dict[str, str]:
        return {k.content: v.content for k, v in self._raw.meta_line.arguments.kwargs}

    @property
    def arguments(self) -> Arguments:
        return Arguments(self._raw.meta_line.arguments)

    @property
    def line_index(self) -> Optional[int]:
        return self._raw.meta_line.line_index

    @property
    def is_void(self) -> bool:
        return self._raw.meta_line.is_void

    def to_json(self) -> str:
        if hasattr(self._raw, 'to_json'):
            return self._raw.to_json()
        raise NotImplementedError("to_json not available for manually created events")

    def __repr__(self) -> str:
        id_part = f' :{self.id}' if self.id else ''
        text_part = f' .. {self.text!r}' if self.text else ''
        return f'BlockStarted(.{self.type_name}{id_part}{text_part})'


class BlockFinished:
    """Event emitted when a block ends."""
    __slots__ = ('_raw',)

    def __init__(self, raw=None):
        self._raw = raw

    @classmethod
    def create(cls) -> 'BlockFinished':
        """Create a BlockFinished event directly."""
        return cls(None)

    def clone(self) -> 'BlockFinished':
        """Create a copy (BlockFinished has no fields to override)."""
        return BlockFinished(None)

    def to_json(self) -> str:
        if self._raw is not None and hasattr(self._raw, 'to_json'):
            return self._raw.to_json()
        return '{"type": "block_finished"}'

    def __repr__(self) -> str:
        return 'BlockFinished()'


class Text:
    """Event emitted when text content is encountered."""
    __slots__ = ('_raw',)

    def __init__(self, raw):
        self._raw = raw

    @classmethod
    def create(cls, content: str | list[str]) -> 'Text':
        """Create a Text event directly. Accepts string or list of lines."""
        if isinstance(content, str):
            lines = content.split('\n')
        else:
            lines = list(content)
        return cls(_TextData(lines=lines, links=[], footnotes=[]))

    def clone(self, *, content: str = None, lines: list[str] = None) -> 'Text':
        """Create a copy with optional field overrides."""
        if content is not None:
            new_lines = content.split('\n')
        elif lines is not None:
            new_lines = list(lines)
        else:
            new_lines = list(self._raw.lines)
        return Text(_TextData(lines=new_lines, links=[], footnotes=[]))

    @property
    def lines(self) -> list[str]:
        return self._raw.lines

    @property
    def content(self) -> str:
        return '\n'.join(self._raw.lines)

    @property
    def text(self) -> str:
        return self.content

    @property
    def links(self) -> list[list[Link]]:
        return [[Link(l) for l in line_links] for line_links in self._raw.links]

    @property
    def all_links(self) -> list[Link]:
        return [Link(l) for line_links in self._raw.links for l in line_links]

    @property
    def footnotes(self) -> list[Footnote]:
        return [Footnote(f) for f in self._raw.footnotes]

    def links_for_line(self, line_index: int) -> list[Link]:
        if 0 <= line_index < len(self._raw.links):
            return [Link(l) for l in self._raw.links[line_index]]
        return []

    def to_json(self) -> str:
        if hasattr(self._raw, 'to_json'):
            return self._raw.to_json()
        import json
        return json.dumps({"type": "text", "lines": self._raw.lines})

    def __repr__(self) -> str:
        preview = self.content[:50] + '...' if len(self.content) > 50 else self.content
        return f'Text({preview!r})'

    def __str__(self) -> str:
        return self.content

    def __iter__(self):
        return iter(self.lines)

    def __len__(self) -> int:
        return len(self.lines)


class ContextLine:
    """Event for context lines (e.g., `./set foo=bar`)."""
    __slots__ = ('_raw',)

    def __init__(self, raw):
        self._raw = raw

    @classmethod
    def create(cls, type_name: str, *, text: str = '', args: Optional[list[str]] = None,
               kwargs: Optional[dict[str, str]] = None) -> 'ContextLine':
        """Create a ContextLine event directly."""
        meta = MetaLine.create(type_name, text=text, args=args, kwargs=kwargs)
        return cls(_ContextLineData(meta_line=meta._raw))

    def clone(self, *, type_name: str = None, text: str = None,
              args: list[str] = None, kwargs: dict[str, str] = None) -> 'ContextLine':
        """Create a copy with optional field overrides."""
        meta = self.meta.clone(type_name=type_name, text=text, args=args, kwargs=kwargs)
        return ContextLine(_ContextLineData(meta_line=meta._raw))

    @property
    def meta(self) -> MetaLine:
        return MetaLine(self._raw.meta_line)

    @property
    def meta_line(self) -> MetaLine:
        return self.meta

    @property
    def type_name(self) -> str:
        return self._raw.meta_line.type_name

    @property
    def text(self) -> str:
        return self._raw.meta_line.inline_text.content

    @property
    def args(self) -> list[str]:
        return [a.content for a in self._raw.meta_line.arguments.args]

    @property
    def kwargs(self) -> dict[str, str]:
        return {k.content: v.content for k, v in self._raw.meta_line.arguments.kwargs}

    def to_json(self) -> str:
        if hasattr(self._raw, 'to_json'):
            return self._raw.to_json()
        raise NotImplementedError("to_json not available for manually created events")

    def __repr__(self) -> str:
        return f'ContextLine(./{self.type_name})'


class VoidElement:
    """Event for void elements (e.g., `.hr:`)."""
    __slots__ = ('_raw',)

    def __init__(self, raw):
        self._raw = raw

    @classmethod
    def create(cls, type_name: str, *, id: Optional[str] = None,
               args: Optional[list[str]] = None,
               kwargs: Optional[dict[str, str]] = None) -> 'VoidElement':
        """Create a VoidElement event directly."""
        meta = MetaLine.create(type_name, args=args, kwargs=kwargs, id=id, is_void=True)
        return cls(_VoidElementData(meta_line=meta._raw))

    def clone(self, *, type_name: str = None, id: str = None,
              args: list[str] = None, kwargs: dict[str, str] = None) -> 'VoidElement':
        """Create a copy with optional field overrides."""
        meta = self.meta.clone(type_name=type_name, id=id, args=args, kwargs=kwargs, is_void=True)
        return VoidElement(_VoidElementData(meta_line=meta._raw))

    @property
    def meta(self) -> MetaLine:
        return MetaLine(self._raw.meta_line)

    @property
    def meta_line(self) -> MetaLine:
        return self.meta

    @property
    def type_name(self) -> str:
        return self._raw.meta_line.type_name

    @property
    def type(self) -> str:
        return self._raw.meta_line.type_name

    @property
    def id(self) -> Optional[str]:
        return self._raw.meta_line.block_id

    @property
    def args(self) -> list[str]:
        return [a.content for a in self._raw.meta_line.arguments.args]

    @property
    def kwargs(self) -> dict[str, str]:
        return {k.content: v.content for k, v in self._raw.meta_line.arguments.kwargs}

    def to_json(self) -> str:
        if hasattr(self._raw, 'to_json'):
            return self._raw.to_json()
        raise NotImplementedError("to_json not available for manually created events")

    def __repr__(self) -> str:
        return f'VoidElement(.{self.type_name}:)'


# Type alias
Event = BlockStarted | BlockFinished | Text | ContextLine | VoidElement


# =============================================================================
# Parsing Functions
# =============================================================================

def _wrap_event(raw_event) -> Event:
    """Convert a raw Rust event to a wrapped Python event."""
    if isinstance(raw_event, _RawBlockStarted):
        return BlockStarted(raw_event)
    elif isinstance(raw_event, _RawBlockFinished):
        return BlockFinished(raw_event)
    elif isinstance(raw_event, _RawText):
        return Text(raw_event)
    elif isinstance(raw_event, _RawContextLine):
        return ContextLine(raw_event)
    elif isinstance(raw_event, _RawVoidElement):
        return VoidElement(raw_event)
    else:
        raise TypeError(f"Unknown event type: {type(raw_event)}")


def _iter_events_from_source(source: Union[str, Path]) -> Iterator[Event]:
    """
    Internal: iterate events from source (string or path).
    """
    # Determine if source is file path or content
    is_file = False
    if isinstance(source, Path):
        is_file = True
    elif isinstance(source, str):
        is_file = (
            '\n' not in source and
            not source.strip().startswith('.') and
            len(source) < 260 and
            ('/' in source or '\\' in source or source.endswith('.wasot'))
        )

    if is_file:
        path = Path(source) if isinstance(source, str) else source
        if not path.exists():
            raise FileNotFoundError(f"Wasot file not found: {path}")
        raw_iter = _parse_wasot_file(str(path), None)
    else:
        raw_iter = _parse_wasot_string(str(source), None)

    for raw_event in raw_iter:
        yield _wrap_event(raw_event)


def iter_events(source: Union[str, Path]) -> Iterator[Event]:
    """
    Iterate over parsing events from Wasot content.

    Args:
        source: Wasot content string or file path

    Yields:
        Event objects (BlockStarted, Text, BlockFinished, ContextLine, VoidElement)

    Example:
        import pywasot

        for event in pywasot.iter_events(".section\\nHello"):
            match event:
                case pywasot.BlockStarted():
                    print(f"Block: {event.type_name}")
                case pywasot.Text():
                    print(f"Text: {event.content}")
    """
    yield from _iter_events_from_source(source)


def iter_events_string(content: str) -> Iterator[Event]:
    """Iterate events from a Wasot string."""
    for raw_event in _parse_wasot_string(content, None):
        yield _wrap_event(raw_event)


def iter_events_file(file_path: Union[str, Path]) -> Iterator[Event]:
    """Iterate events from a Wasot file."""
    for raw_event in _parse_wasot_file(str(file_path), None):
        yield _wrap_event(raw_event)


# Legacy aliases
parse_string = iter_events_string
parse_file = iter_events_file
raw_parse_wasot = iter_events
parse_wasot_string = iter_events_string
parse_wasot_file = iter_events_file


# =============================================================================
# Parser Base Class
# =============================================================================

class Parser(ABC):
    """Abstract base class for event-driven Wasot parsers."""

    def __init__(self, selectors: list[str] = None):
        self.selectors = selectors or []

    def parse_file(self, file_path: Union[str, Path]):
        for event in iter_events_file(file_path):
            match event:
                case BlockStarted():
                    self.on_block_started(event)
                case BlockFinished():
                    self.on_block_finished(event)
                case Text():
                    self.on_text(event)
                case ContextLine():
                    self.on_context_line(event)
                case VoidElement():
                    self.on_void_element(event)

    def on_block_started(self, event: BlockStarted):
        pass

    def on_text(self, event: Text):
        pass

    def on_block_finished(self, event: BlockFinished):
        pass

    def on_context_line(self, event: ContextLine):
        pass

    def on_void_element(self, event: VoidElement):
        pass


# =============================================================================
# Tree API - Reconstruct tree structure from events
# =============================================================================

from typing import TextIO


class Node:
    """
    Base class for all tree nodes.

    Provides BeautifulSoup-like navigation properties.
    """
    __slots__ = ('_parent',)

    def __init__(self):
        self._parent: Optional['Block'] = None

    @property
    def parent(self) -> Optional['Block']:
        """Get parent block (BS4-style)."""
        return self._parent

    @property
    def parents(self) -> Iterator['Block']:
        """Iterate over all ancestor blocks (BS4-style alias for ancestors)."""
        node = self._parent
        while node is not None:
            yield node
            node = node._parent

    @property
    def ancestors(self) -> Iterator['Block']:
        """Iterate over ancestor blocks from parent to root."""
        return self.parents

    @property
    def root(self) -> 'Block':
        """Get the root block."""
        node = self
        while node._parent is not None:
            node = node._parent
        return node

    def _get_siblings(self) -> tuple[list['Node'], int]:
        """Get siblings list and own index."""
        if self._parent is None:
            return [], -1
        siblings = self._parent._children
        try:
            idx = siblings.index(self)
            return siblings, idx
        except ValueError:
            return siblings, -1

    @property
    def next_sibling(self) -> Optional['Node']:
        """Get next sibling node (BS4-style)."""
        siblings, idx = self._get_siblings()
        if idx >= 0 and idx < len(siblings) - 1:
            return siblings[idx + 1]
        return None

    @property
    def previous_sibling(self) -> Optional['Node']:
        """Get previous sibling node (BS4-style)."""
        siblings, idx = self._get_siblings()
        if idx > 0:
            return siblings[idx - 1]
        return None

    @property
    def next_siblings(self) -> Iterator['Node']:
        """Iterate over following siblings (BS4-style)."""
        siblings, idx = self._get_siblings()
        if idx >= 0:
            for i in range(idx + 1, len(siblings)):
                yield siblings[i]

    @property
    def previous_siblings(self) -> Iterator['Node']:
        """Iterate over preceding siblings in reverse (BS4-style)."""
        siblings, idx = self._get_siblings()
        if idx > 0:
            for i in range(idx - 1, -1, -1):
                yield siblings[i]

    @property
    def next_element(self) -> Optional['Node']:
        """Get next element in document order (BS4-style)."""
        # First try next sibling
        if self.next_sibling:
            return self.next_sibling
        # Then try parent's next sibling
        for ancestor in self.parents:
            if ancestor.next_sibling:
                return ancestor.next_sibling
        return None

    @property
    def previous_element(self) -> Optional['Node']:
        """Get previous element in document order (BS4-style)."""
        if self.previous_sibling:
            # Get the last descendant of previous sibling
            prev = self.previous_sibling
            if isinstance(prev, Block) and prev._children:
                while isinstance(prev, Block) and prev._children:
                    prev = prev._children[-1]
            return prev
        return self._parent


class Block(Node):
    """
    Block node in the tree. Wraps a BlockStarted event.

    Provides tree navigation and access to all block properties.
    """
    __slots__ = ('_event', '_children', '_end_event')

    def __init__(self, event: BlockStarted, end_event: Optional[BlockFinished] = None):
        super().__init__()
        self._event = event
        self._children: list[Node] = []
        self._end_event = end_event

    @classmethod
    def create(cls, type_name: str, *, level: int = 1, text: str = '',
               id: Optional[str] = None, args: Optional[list[str]] = None,
               kwargs: Optional[dict[str, str]] = None) -> 'Block':
        """Create a Block directly without parsing."""
        event = BlockStarted.create(type_name, level=level, text=text,
                                    id=id, args=args, kwargs=kwargs)
        block = cls(event)
        block._end_event = BlockFinished.create()
        return block

    # ===== Properties delegated to event =====

    @property
    def event(self) -> BlockStarted:
        """Get the underlying BlockStarted event."""
        return self._event

    @property
    def end_event(self) -> Optional[BlockFinished]:
        """Get the BlockFinished event that closes this block."""
        return self._end_event

    def iter_events(self) -> Iterator[Event]:
        """
        Iterate all events in this block in depth-first order.

        Yields:
            BlockStarted for this block, then all child events recursively,
            then BlockFinished for this block.

        Example:
            for event in block.iter_events():
                print(event)
        """
        yield self._event
        for child in self._children:
            if isinstance(child, Block):
                yield from child.iter_events()
            elif isinstance(child, TextNode):
                yield child._event
            elif isinstance(child, Void):
                yield child._event
            elif isinstance(child, Directive):
                yield child._event
        if self._end_event:
            yield self._end_event

    def to_events(self) -> Iterator[Event]:
        """
        Return an iterator of all events in depth-first order.

        Alias for iter_events(). Consistent with to_jsonl(), to_dict(), etc.

        Example:
            for event in block.to_events():
                print(event)
        """
        return self.iter_events()

    def to_jsonl(self) -> Iterator[str]:
        for event in self.iter_events():
            yield event.to_json()

    @property
    def type_name(self) -> str:
        return self._event.type_name

    @property
    def type(self) -> str:
        return self._event.type_name

    @property
    def name(self) -> str:
        """Block name/type (BS4-style alias for type_name)."""
        return self._event.type_name

    @property
    def level(self) -> int:
        return self._event.level

    @property
    def id(self) -> Optional[str]:
        return self._event.id

    @property
    def args(self) -> list[str]:
        return self._event.args

    @property
    def kwargs(self) -> dict[str, str]:
        return self._event.kwargs

    @property
    def inline_text(self) -> str:
        """Get inline text from the metaline."""
        return self._event.text

    # ===== Children access =====

    @property
    def children(self) -> list[Node]:
        """Get all child nodes."""
        return self._children

    @property
    def blocks(self) -> list['Block']:
        """Get child blocks only."""
        return [c for c in self._children if isinstance(c, Block)]

    @property
    def text_nodes(self) -> list['TextNode']:
        """Get child text nodes only."""
        return [c for c in self._children if isinstance(c, TextNode)]

    @property
    def void_nodes(self) -> list['Void']:
        """Get child void elements only."""
        return [c for c in self._children if isinstance(c, Void)]

    @property
    def directives(self) -> list['Directive']:
        """Get child directives (context lines) only."""
        return [c for c in self._children if isinstance(c, Directive)]

    # ===== Text content =====

    @property
    def text(self) -> str:
        """Get all text content (inline + text nodes), joined with newlines."""
        parts = []
        if self.inline_text:
            parts.append(self.inline_text)
        for child in self._children:
            if isinstance(child, TextNode):
                parts.append(child.content)
            elif isinstance(child, Block):
                child_text = child.text
                if child_text:
                    parts.append(child_text)
        return '\n'.join(parts)

    @property
    def direct_text(self) -> str:
        """Get only direct text content (inline + immediate text nodes)."""
        parts = []
        if self.inline_text:
            parts.append(self.inline_text)
        for child in self._children:
            if isinstance(child, TextNode):
                parts.append(child.content)
        return '\n'.join(parts)

    # ===== BeautifulSoup-style text access =====

    @property
    def string(self) -> Optional[str]:
        """
        Get string if block contains only one text element (BS4-style).

        Returns None if there are multiple text elements or nested blocks.
        """
        texts = []
        if self.inline_text:
            texts.append(self.inline_text)
        for child in self._children:
            if isinstance(child, TextNode):
                texts.append(child.content)
            elif isinstance(child, Block):
                return None  # Has nested blocks
        if len(texts) == 1:
            return texts[0]
        return None

    @property
    def strings(self) -> Iterator[str]:
        """Iterate over all strings in this block (BS4-style)."""
        if self.inline_text:
            yield self.inline_text
        for child in self._children:
            if isinstance(child, TextNode):
                yield child.content
            elif isinstance(child, Block):
                yield from child.strings

    @property
    def stripped_strings(self) -> Iterator[str]:
        """Iterate over all strings, stripping whitespace (BS4-style)."""
        for s in self.strings:
            stripped = s.strip()
            if stripped:
                yield stripped

    def get_text(self, separator: str = '', strip: bool = False) -> str:
        """
        Get all text content with options (BS4-style).

        Args:
            separator: String to join text pieces (default: '')
            strip: Whether to strip whitespace from each piece

        Example:
            block.get_text()           # All text concatenated
            block.get_text(' ')        # Space-separated
            block.get_text('\\n', strip=True)  # Lines, stripped
        """
        if strip:
            return separator.join(self.stripped_strings)
        return separator.join(self.strings)

    # ===== BeautifulSoup-style traversal =====

    @property
    def descendants(self) -> Iterator['Node']:
        """Iterate over all descendants (BS4-style depth-first)."""
        for child in self._children:
            yield child
            if isinstance(child, Block):
                yield from child.descendants

    # ===== Tree navigation =====

    def find(self, type_name: str = None, **kwargs) -> Optional['Block']:
        """
        Find first descendant block matching criteria.

        Args:
            type_name: Block type to match (e.g., 'paragraph', 'section')
            **kwargs: Attribute filters (e.g., id='intro')

        Returns:
            First matching Block or None
        """
        for child in self._children:
            if isinstance(child, Block):
                if self._matches(child, type_name, kwargs):
                    return child
                result = child.find(type_name, **kwargs)
                if result:
                    return result
        return None

    def find_all(self, type_name: str = None, **kwargs) -> list['Block']:
        """
        Find all descendant blocks matching criteria.

        Args:
            type_name: Block type to match
            **kwargs: Attribute filters

        Returns:
            List of matching Blocks
        """
        results = []
        for child in self._children:
            if isinstance(child, Block):
                if self._matches(child, type_name, kwargs):
                    results.append(child)
                results.extend(child.find_all(type_name, **kwargs))
        return results

    @staticmethod
    def _matches(block: 'Block', type_name: Optional[str], attrs: dict) -> bool:
        """Check if block matches criteria."""
        if type_name and block.type_name != type_name:
            return False
        for key, value in attrs.items():
            if key == 'id':
                if block.id != value:
                    return False
            elif block.kwargs.get(key) != value:
                return False
        return True

    # ===== Mutation =====

    def append(self, node: Node) -> None:
        """Add a child node."""
        node._parent = self
        self._children.append(node)

    def insert(self, index: int, node: Node) -> None:
        """Insert a child node at index."""
        node._parent = self
        self._children.insert(index, node)

    def remove(self, node: Node) -> None:
        """Remove a child node."""
        self._children.remove(node)
        node._parent = None

    # ===== Conversion =====

    def to_dict(self) -> dict[str, Any]:
        """Convert to dict structure (compatible with loads/dumps)."""
        result = {
            'name': self.type_name,
            'args': self.args,
            'kwargs': self.kwargs,
            'text': self.direct_text,
            'children': [],
        }
        for child in self._children:
            if isinstance(child, Block):
                result['children'].append(child.to_dict())
        return result

    def to_wasot(self, level: int = 0) -> str:
        """Convert to Wasot format string."""
        lines = []
        dots = '.' * (level + 1)
        meta_line = f"{dots}{self.type_name}"

        for arg in self.args:
            meta_line += f' "{arg}"' if ' ' in arg else f' {arg}'
        for key, value in self.kwargs.items():
            meta_line += f' {key}="{value}"' if ' ' in str(value) else f' {key}={value}'

        if self.inline_text and not self._children:
            meta_line += f' .. {self.inline_text}'
            lines.append(meta_line)
        else:
            lines.append(meta_line)
            for child in self._children:
                if isinstance(child, TextNode):
                    lines.append(child.content)
                elif isinstance(child, Block):
                    lines.append(child.to_wasot(level + 1))
                elif isinstance(child, Void):
                    lines.append(child.to_wasot(level + 1))
            if self.blocks:
                lines.append(f"{dots}/")

        return '\n'.join(lines)

    # ===== Dict/List-like access =====

    def __iter__(self) -> Iterator[Node]:
        return iter(self._children)

    def __len__(self) -> int:
        return len(self._children)

    def __getitem__(self, key):
        """
        Access children by index or properties by name.

        Examples:
            block[0]           # First child
            block[-1]          # Last child
            block[0:2]         # Slice of children
            block['name']      # Block type name
            block['text']      # All text content
            block['value']     # Block's value (args or text)
            block['children']  # Children list
            block['args']      # Positional arguments
            block['kwargs']    # Keyword arguments dict
            block['id']        # Block ID
            block['key']       # Kwargs value, or first child block with that type
        """
        if isinstance(key, int):
            return self._children[key]
        elif isinstance(key, slice):
            return self._children[key]
        elif isinstance(key, str):
            # First, try finding a child block with that type_name
            for child in self._children:
                if isinstance(child, Block) and child.type_name == key:
                    return child
            # Then try kwargs
            if key in self.kwargs:
                return self.kwargs[key]
            # Finally, special properties (use attributes for these if ambiguous)
            if key == '_name' or key == 'type_name':
                return self.type_name
            elif key == '_text':
                return self.text
            elif key == '_value':
                return self.value
            elif key == '_children' or key == 'children':
                return self._children
            elif key == '_args' or key == 'args':
                return self.args
            elif key == '_kwargs' or key == 'kwargs':
                return self.kwargs
            elif key == '_id':
                return self.id
            elif key == '_blocks' or key == 'blocks':
                return self.blocks
            raise KeyError(key)
        raise TypeError(f"Key must be int, slice, or str, not {type(key).__name__}")

    def __setitem__(self, key, value):
        """Set child by index or attribute by name."""
        if isinstance(key, int):
            if isinstance(value, Node):
                value._parent = self
            self._children[key] = value
        elif isinstance(key, str):
            # For string keys, we could store in a separate dict
            # but for now raise an error for immutable properties
            raise TypeError("Block attributes are read-only. Use kwargs for custom data.")
        else:
            raise TypeError(f"Key must be int or str, not {type(key).__name__}")

    def __delitem__(self, key):
        """Delete child by index."""
        if isinstance(key, int):
            child = self._children.pop(key)
            child._parent = None
        else:
            raise TypeError(f"Can only delete by int index, not {type(key).__name__}")

    def __contains__(self, key) -> bool:
        """
        Check if key exists.

        For strings: checks kwargs, then child type_names.
        For nodes: checks if node is a child.
        """
        if isinstance(key, str):
            # Check child blocks first
            if any(isinstance(c, Block) and c.type_name == key for c in self._children):
                return True
            # Then kwargs
            if key in self.kwargs:
                return True
            # Then special properties
            if key in ('_name', 'type_name', '_text', '_value', '_children', 'children',
                       '_args', 'args', '_kwargs', 'kwargs', '_id', '_blocks', 'blocks'):
                return True
            return False
        elif isinstance(key, Node):
            return key in self._children
        return False

    def get(self, key: str, default=None):
        """Get property with default value."""
        try:
            return self[key]
        except KeyError:
            return default

    def keys(self) -> list[str]:
        """Return available string keys (child types, kwargs, then special)."""
        keys = []
        # Child block type names first
        for child in self._children:
            if isinstance(child, Block) and child.type_name not in keys:
                keys.append(child.type_name)
        # Then kwargs
        for k in self.kwargs.keys():
            if k not in keys:
                keys.append(k)
        # Then special properties (prefixed to avoid conflicts)
        keys.extend(['children', 'blocks', 'args', 'kwargs'])
        return keys

    def values(self):
        """Return values for all keys."""
        return [self[k] for k in self.keys()]

    def items(self):
        """Return (key, value) pairs."""
        return [(k, self[k]) for k in self.keys()]


    # ===== CSS-like selectors (BS4-style) =====

    def find(self, selector: str) -> list['Block']:
        """
        Find all blocks matching a CSS-like selector (BS4-style).

        Supported selectors:
            'type'           - Match by type name
            '#id'            - Match by ID
            'type#id'        - Match both
            'parent child'   - Descendant (any depth)
            'parent > child' - Direct child only

        Example:
            doc.select('section')           # All sections
            doc.select('section > paragraph')  # Direct children
            doc.select('#intro')            # By ID
            doc.select('section paragraph') # Nested

        Returns:
            List of matching Block nodes
        """
        return self._select(selector.strip())

    def _select(self, selector: str) -> list['Block']:
        """Internal selector implementation."""
        # Handle descendant combinator (space)
        if ' > ' in selector:
            # Direct child combinator
            parts = selector.split(' > ', 1)
            parent_sel, child_sel = parts[0].strip(), parts[1].strip()
            results = []
            for parent in self._select(parent_sel):
                for child in parent._children:
                    if isinstance(child, Block) and child._matches_simple(child_sel):
                        results.append(child)
            return results
        elif ' ' in selector:
            # Descendant combinator
            parts = selector.split(None, 1)
            parent_sel, child_sel = parts[0], parts[1]
            results = []
            for parent in self._select(parent_sel):
                results.extend(parent._select(child_sel))
            return results
        else:
            # Simple selector
            return [b for b in self._all_blocks() if b._matches_simple(selector)]

    def _all_blocks(self) -> Iterator['Block']:
        """Iterate all descendant blocks."""
        for child in self._children:
            if isinstance(child, Block):
                yield child
                yield from child._all_blocks()

    def _matches_simple(self, selector: str) -> bool:
        """Check if this block matches a simple selector (type, #id, or type#id)."""
        if not selector:
            return True
        if selector.startswith('#'):
            # ID only
            return self.id == selector[1:]
        elif '#' in selector:
            # Type and ID
            type_part, id_part = selector.split('#', 1)
            return self.type_name == type_part and self.id == id_part
        else:
            # Type only
            return self.type_name == selector

    def find_one(self, selector: str) -> Optional['Block']:
        """
        Find first block matching a CSS-like selector (BS4-style).

        Example:
            doc.select_one('section#intro')  # First matching

        Returns:
            First matching Block or None
        """
        results = self.find(selector)
        return results[0] if results else None

    def __repr__(self) -> str:
        id_part = f' :{self.id}' if self.id else ''
        children_part = f' [{len(self._children)} children]' if self._children else ''
        return f"Block(.{self.type_name}{id_part}{children_part})"

    def __str__(self) -> str:
        """
        Format all events in depth-first order.

        Returns a string representation showing the event stream
        that would recreate this block structure.
        """
        lines = []
        indent = 0
        for event in self.iter_events():
            if isinstance(event, BlockStarted):
                id_part = f' :{event.id}' if event.id else ''
                args_part = ' ' + ' '.join(event.args) if event.args else ''
                lines.append(f"{'  ' * indent}BlockStarted(.{event.type_name}{id_part}{args_part})")
                indent += 1
            elif isinstance(event, Text):
                content = event.content
                if '\n' in content:
                    preview = content.split('\n')[0][:40] + '...'
                else:
                    preview = content[:50]
                lines.append(f"{'  ' * indent}Text({preview!r})")
            elif isinstance(event, BlockFinished):
                indent = max(0, indent - 1)
                lines.append(f"{'  ' * indent}BlockFinished()")
            elif isinstance(event, VoidElement):
                lines.append(f"{'  ' * indent}VoidElement(.{event.type_name}:)")
            elif isinstance(event, ContextLine):
                lines.append(f"{'  ' * indent}ContextLine(./{event.type_name})")
        return '\n'.join(lines)


class TextNode(Node):
    """
    Text content node in the tree. Wraps a Text event.
    """
    __slots__ = ('_event',)

    def __init__(self, event: Text):
        super().__init__()
        self._event = event

    @classmethod
    def create(cls, content: str | list[str]) -> 'TextNode':
        """Create a TextNode directly."""
        event = Text.create(content)
        return cls(event)

    @property
    def event(self) -> Text:
        """Get the underlying Text event."""
        return self._event

    @property
    def content(self) -> str:
        return self._event.content

    @property
    def lines(self) -> list[str]:
        return self._event.lines

    @property
    def links(self) -> list[list[Link]]:
        return self._event.links

    @property
    def all_links(self) -> list[Link]:
        return self._event.all_links

    @property
    def footnotes(self) -> list[Footnote]:
        return self._event.footnotes

    def __str__(self) -> str:
        return self.content

    def __repr__(self) -> str:
        preview = self.content[:40] + '...' if len(self.content) > 40 else self.content
        return f"TextNode({preview!r})"


class Void(Node):
    """
    Void (self-closing) element in the tree. Wraps a VoidElement event.
    """
    __slots__ = ('_event',)

    def __init__(self, event: VoidElement):
        super().__init__()
        self._event = event

    @classmethod
    def create(cls, type_name: str, *, id: Optional[str] = None,
               args: Optional[list[str]] = None,
               kwargs: Optional[dict[str, str]] = None) -> 'Void':
        """Create a Void node directly."""
        event = VoidElement.create(type_name, id=id, args=args, kwargs=kwargs)
        return cls(event)

    @property
    def event(self) -> VoidElement:
        return self._event

    @property
    def type_name(self) -> str:
        return self._event.type_name

    @property
    def type(self) -> str:
        return self._event.type_name

    @property
    def id(self) -> Optional[str]:
        return self._event.id

    @property
    def args(self) -> list[str]:
        return self._event.args

    @property
    def kwargs(self) -> dict[str, str]:
        return self._event.kwargs

    def to_wasot(self, level: int = 0) -> str:
        dots = '.' * (level + 1)
        return f"{dots}{self.type_name}:"

    def __repr__(self) -> str:
        return f"Void(.{self.type_name}:)"


class Directive(Node):
    """
    Directive (context line) in the tree. Wraps a ContextLine event.

    Directives are metadata/commands within blocks (e.g., ./set key=value).
    """
    __slots__ = ('_event',)

    def __init__(self, event: ContextLine):
        super().__init__()
        self._event = event

    @classmethod
    def create(cls, type_name: str, *, text: str = '',
               args: Optional[list[str]] = None,
               kwargs: Optional[dict[str, str]] = None) -> 'Directive':
        """Create a Directive directly."""
        event = ContextLine.create(type_name, text=text, args=args, kwargs=kwargs)
        return cls(event)

    @property
    def event(self) -> ContextLine:
        return self._event

    @property
    def type_name(self) -> str:
        return self._event.type_name

    @property
    def text(self) -> str:
        return self._event.text

    @property
    def args(self) -> list[str]:
        return self._event.args

    @property
    def kwargs(self) -> dict[str, str]:
        return self._event.kwargs

    def __repr__(self) -> str:
        return f"Directive(./{self.type_name})"


# Type alias for tree nodes
TreeNode = Block | TextNode | Void | Directive


def _build_tree(events: Iterator[Event]) -> Block:
    """Internal: build a tree from events."""
    root = Block.create('_root')
    stack: list[Block] = [root]

    for event in events:
        match event:
            case BlockStarted():
                if event.type_name == '/':
                    continue
                block = Block(event)
                stack[-1].append(block)
                stack.append(block)

            case Text():
                # Skip empty text (whitespace-only at root level)
                if event.content.strip() or len(stack) > 1:
                    node = TextNode(event)
                    stack[-1].append(node)

            case VoidElement():
                node = Void(event)
                stack[-1].append(node)

            case ContextLine():
                node = Directive(event)
                stack[-1].append(node)

            case BlockFinished():
                if len(stack) > 1:
                    finished_block = stack.pop()
                    finished_block._end_event = event

    # Unwrap synthetic root if there's a single block child
    if len(root.children) == 1 and isinstance(root.children[0], Block):
        result = root.children[0]
        result._parent = None  # Clear parent reference to synthetic root
        return result
    # If multiple root elements, return the synthetic root renamed as 'document'
    root._event = BlockStarted.create('document')
    return root


def parse(source: Union[str, Path, TextIO]) -> Block:
    """
    Parse Wasot content into a tree structure.

    This is the main parsing function. It handles:
    - String content
    - File paths (str or Path)
    - File objects (TextIO)

    Args:
        source: Wasot content string, file path, or file object

    Returns:
        Root Block containing the document tree

    Example:
        import pywasot

        # From string
        doc = pywasot.parse('''
        .document
        ..section :intro
        ...paragraph
        Hello world
        ''')

        # From file path
        doc = pywasot.parse('document.wasot')
        doc = pywasot.parse(Path('document.wasot'))

        # From file object
        with open('document.wasot') as f:
            doc = pywasot.parse(f)

        # Navigate the tree
        print(doc.type_name)  # 'document'
        section = doc.find('section', id='intro')
        print(section.text)  # 'Hello world'

        # Access children via [] or .get()
        print(doc['section']['paragraph'].text)
        print(doc.get('section'))
    """
    # Handle file objects
    if hasattr(source, 'read'):
        content = source.read()
        return _build_tree(iter_events_string(content))

    return _build_tree(_iter_events_from_source(source))


def tree(source: Union[str, Path, TextIO]) -> Block:
    """Alias for parse(). Parse Wasot content into a tree structure."""
    return parse(source)


def tree_string(content: str) -> Block:
    """Parse Wasot string into a tree."""
    return _build_tree(iter_events_string(content))


def tree_file(path: Union[str, Path]) -> Block:
    """Parse Wasot file into a tree."""
    return _build_tree(iter_events_file(path))


# Document is just a type alias for Block.
# Use pywasot.parse() to create a document from content.
Document = Block


# =============================================================================
# JSON-like API
# =============================================================================


def loads(s: str) -> dict[str, Any]:
    """
    Load Wasot from a string.

    Args:
        s: Wasot content as string

    Returns:
        Dict with structure: {
            'name': str,           # Block type name
            'args': list,          # Positional arguments
            'kwargs': dict,        # Keyword arguments
            'text': str,           # All text content joined
            'children': list,      # Child blocks (dicts)
        }

    Example:
        data = pywasot.loads('.block .. key=value\\nHello world')
        print(data['name'])     # 'block'
        print(data['text'])     # 'Hello world'
        print(data['kwargs'])   # {'key': 'value'}
    """
    stack: list[dict[str, Any]] = []
    root: dict[str, Any] = {'name': 'document', 'args': [], 'kwargs': {}, 'text': '', 'children': []}
    current_text: list[str] = []

    def flush_text():
        nonlocal current_text
        if current_text:
            text = '\n'.join(current_text)
            if stack:
                if stack[-1]['text']:
                    stack[-1]['text'] += '\n' + text
                else:
                    stack[-1]['text'] = text
            else:
                if root['text']:
                    root['text'] += '\n' + text
                else:
                    root['text'] = text
            current_text = []

    for event in iter_events_string(s):
        if isinstance(event, BlockStarted):
            flush_text()

            # Skip closing blocks
            if event.type_name == '/':
                continue

            block = {
                'name': event.type_name,
                'args': event.args,
                'kwargs': event.kwargs,
                'text': event.text or '',
                'children': [],
            }

            if stack:
                stack[-1]['children'].append(block)
            else:
                root['children'].append(block)

            stack.append(block)

        elif isinstance(event, Text):
            current_text.extend(event.lines)

        elif isinstance(event, BlockFinished):
            flush_text()
            if stack:
                stack.pop()

    flush_text()

    # Return single block if document has only one child
    if len(root['children']) == 1:
        return root['children'][0]
    return root


def load(fp: TextIO) -> dict[str, Any]:
    """
    Load Wasot from a file object.

    Args:
        fp: File object opened for reading

    Returns:
        Dict structure (see loads())

    Example:
        with open('document.wasot') as f:
            data = pywasot.load(f)
            print(data['name'])
    """
    return loads(fp.read())


def dumps(obj: Union[dict, list, str]) -> str:
    """
    Convert data structure to Wasot string.

    Args:
        obj: Dict or list to convert

    Returns:
        Wasot format string

    Example:
        data = pywasot.loads('.article\\nHello')
        text = pywasot.dumps(data)
    """
    output: list[str] = []

    def process_element(element: Any, level: int):
        if isinstance(element, str):
            if element:
                output.append(element)
        elif isinstance(element, dict):
            process_block(element, level)
        elif isinstance(element, list):
            for item in element:
                process_element(item, level)

    def process_block(block: dict, level: int):
        # Build meta-line
        dots = '.' * (level + 1)
        name = block.get('name', 'block')

        meta_line = f"{dots}{name}"

        # Add positional arguments
        for arg in block.get('args', []):
            if ' ' in str(arg):
                meta_line += f' "{arg}"'
            else:
                meta_line += f' {arg}'

        # Add keyword arguments
        for key, value in block.get('kwargs', {}).items():
            if ' ' in str(value):
                meta_line += f' {key}="{value}"'
            else:
                meta_line += f' {key}={value}'

        # Add inline text (from metaline)
        inline = block.get('text', '')
        has_children = bool(block.get('children'))

        # If text and no children, put text inline
        if inline and not has_children:
            meta_line += f' .. {inline}'
            output.append(meta_line)
        else:
            output.append(meta_line)

            # Add text as content
            if inline:
                output.append(inline)

            # Process children
            for child in block.get('children', []):
                process_element(child, level + 1)

            # Add explicit close if has children
            if has_children:
                output.append(f"{dots}/")

    if isinstance(obj, dict) and obj.get('name') == 'document':
        process_element(obj, -1)
    else:
        process_element(obj, 0)

    return '\n'.join(output)


def dump(obj: Union[dict, list], fp: TextIO) -> None:
    """
    Write Wasot to a file object.

    Args:
        obj: Dict or list to write
        fp: File object opened for writing

    Example:
        with open('output.wasot', 'w') as f:
            pywasot.dump(data, f)
    """
    fp.write(dumps(obj))


def load_file(path: Union[str, Path]) -> dict[str, Any]:
    """
    Load Wasot from a file path.

    Args:
        path: Path to Wasot file

    Returns:
        Dict structure (see loads())

    Example:
        data = pywasot.load_file('document.wasot')
        print(data['name'])
    """
    with open(path, 'r') as f:
        return load(f)


def save_file(obj: Union[dict, list], path: Union[str, Path]) -> None:
    """
    Save data structure to Wasot file.

    Args:
        obj: Dict or list to save
        path: Path to output file

    Example:
        pywasot.save_file(data, 'output.wasot')
    """
    with open(path, 'w') as f:
        dump(obj, f)


# =============================================================================
# Exports
# =============================================================================

__all__ = [
    # Core types
    "TargetType",
    "ExternalIdReference",
    "QuotedString",
    "Link",
    "Footnote",
    "Arguments",
    "MetaLine",
    "Context",
    # Events
    "Event",
    "BlockStarted",
    "BlockFinished",
    "Text",
    "ContextLine",
    "VoidElement",
    # Parsing - main API
    "parse",              # Parse to tree (returns Block)
    "iter_events",        # Iterate events
    "iter_events_string", # Iterate events from string
    "iter_events_file",   # Iterate events from file
    "Parser",             # Abstract parser base class
    # Tree API
    "Document",           # Alias for Block
    "Node",
    "Block",
    "TextNode",
    "Void",
    "Directive",
    "TreeNode",
    "tree",               # Alias for parse
    "tree_string",
    "tree_file",
    # Legacy aliases
    "raw_parse_wasot",
    "parse_wasot_string",
    "parse_wasot_file",
    "parse_string",
    "parse_file",
    # Utilities
    "WasotIterator",
    "Specificity",
    "calculate_specificity",
    "parse_metaline",
    # JSON API
    "load",
    "loads",
    "dump",
    "dumps",
    "load_file",
    "save_file"
]
