from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any, Generator, Literal, cast

from ijson import ObjectBuilder  # type: ignore
from ijson.utils import coroutine as _ijson_coroutine  # type: ignore

# Public constants / prefixes
ATTACHMENT_PREFIX = "attachment://"
ATTACHMENT_PREFIX_LEN = len(ATTACHMENT_PREFIX)
ATTACHMENTS_PREFIX = "attachments."
MESSAGES_ITEM_PREFIX = "messages.item"
EVENTS_ITEM_PREFIX = "events.item"
TIMELINES_ITEM_PREFIX = "timelines.item"
MESSAGE_POOL_ITEM_PREFIX = "message_pool.item"
CALL_POOL_ITEM_PREFIX = "call_pool.item"
METADATA_PREFIX = "metadata."


def _should_skip(
    filter_field_value: str, filter_list: None | Literal["all"] | list[str]
) -> bool:
    if filter_list is None:
        return True
    if filter_list == "all":
        return False
    return filter_field_value not in filter_list


@dataclass(frozen=True, slots=True)
class ListProcessingConfig:
    array_item_prefix: str
    filter_field: str
    filter_list: None | Literal["all"] | list[str]
    filter_prefix: str = field(init=False)

    def __post_init__(self) -> None:
        object.__setattr__(
            self, "filter_prefix", f"{self.array_item_prefix}.{self.filter_field}"
        )


@dataclass(slots=True)
class ParseState:
    messages: list[dict[str, Any]] = field(default_factory=list)
    events: list[dict[str, Any]] = field(default_factory=list)
    timelines: list[dict[str, Any]] = field(default_factory=list)
    attachment_refs: set[str] = field(default_factory=set)
    attachments: dict[str, str] = field(default_factory=dict)
    metadata: dict[str, Any] = field(default_factory=dict)
    target: str | list[str] | None = None
    scores: dict[str, Any] = field(default_factory=dict)
    message_pool: list[dict[str, Any]] = field(default_factory=list)
    call_pool: list[dict[str, Any]] = field(default_factory=list)


# ---------------------------------------------------------------------------
# Coroutine-based object processors (idiomatic style with early filtering)
# ---------------------------------------------------------------------------

EventTuple = tuple[str, str, Any]
CoroutineGen = Generator[None, EventTuple, None]


@_ijson_coroutine  # type: ignore
def _item_coroutine(
    target_list: list[dict[str, Any]],
    attachment_refs: set[str],
    config: ListProcessingConfig,
) -> CoroutineGen:  # pragma: no cover
    builder: ObjectBuilder | None = None
    attachments: set[str] = set()
    skip = False
    item_prefix = config.array_item_prefix
    filter_prefix = config.filter_prefix
    while True:
        prefix, event, value = yield
        if prefix == item_prefix and event == "start_map":
            builder = ObjectBuilder()
            builder.event(event, value)
            attachments.clear()
            skip = False
            continue
        if builder is None:
            continue
        if prefix == filter_prefix and event == "string":
            if _should_skip(value, config.filter_list):
                builder = None
                skip = True
                attachments.clear()
                continue
        if prefix == item_prefix and event == "end_map":
            if not skip and builder is not None:
                try:
                    builder.event(event, value)
                    item = builder.value
                    target_list.append(item)
                    attachment_refs.update(attachments)
                except Exception:
                    pass
            builder = None
            skip = False
            attachments.clear()
            continue
        if skip:
            continue
        try:
            builder.event(event, value)
        except Exception:
            builder = None
            continue
        if event == "string" and isinstance(value, str):
            if len(value) == 45 and value.startswith(ATTACHMENT_PREFIX):
                attachments.add(value[ATTACHMENT_PREFIX_LEN:])


def message_item_coroutine(
    state: ParseState, config: ListProcessingConfig
) -> CoroutineGen:
    return cast(
        CoroutineGen, _item_coroutine(state.messages, state.attachment_refs, config)
    )


def event_item_coroutine(
    state: ParseState, config: ListProcessingConfig
) -> CoroutineGen:
    return cast(
        CoroutineGen, _item_coroutine(state.events, state.attachment_refs, config)
    )


@_ijson_coroutine  # type: ignore
def _unfiltered_item_coroutine(
    target_list: list[dict[str, Any]],
    item_prefix: str,
) -> CoroutineGen:  # pragma: no cover
    """Collect items from the JSON stream without filtering."""
    builder: ObjectBuilder | None = None
    while True:
        prefix, event, value = yield
        if prefix == item_prefix and event == "start_map":
            builder = ObjectBuilder()
            builder.event(event, value)
            continue
        if builder is None:
            continue
        if prefix == item_prefix and event == "end_map":
            try:
                builder.event(event, value)
                target_list.append(builder.value)
            except Exception:
                pass
            builder = None
            continue
        try:
            builder.event(event, value)
        except Exception:
            builder = None
            continue


def timeline_item_coroutine(state: ParseState) -> CoroutineGen:
    return cast(
        CoroutineGen,
        _unfiltered_item_coroutine(state.timelines, TIMELINES_ITEM_PREFIX),
    )


def message_pool_item_coroutine(state: ParseState) -> CoroutineGen:
    return cast(
        CoroutineGen,
        _unfiltered_item_coroutine(state.message_pool, MESSAGE_POOL_ITEM_PREFIX),
    )


def call_pool_item_coroutine(state: ParseState) -> CoroutineGen:
    return cast(
        CoroutineGen,
        _unfiltered_item_coroutine(state.call_pool, CALL_POOL_ITEM_PREFIX),
    )


@_ijson_coroutine  # type: ignore
def attachments_coroutine(state: ParseState) -> CoroutineGen:  # pragma: no cover
    attachments_prefix_len = len(ATTACHMENTS_PREFIX)
    while True:
        prefix, event, value = yield
        if event != "string":
            continue
        if not prefix.startswith(ATTACHMENTS_PREFIX):
            continue
        end = prefix.find(".", attachments_prefix_len)
        attachment_id = (
            prefix[attachments_prefix_len:]
            if end == -1
            else prefix[attachments_prefix_len:end]
        )
        if attachment_id in state.attachment_refs:
            state.attachments[attachment_id] = value


@_ijson_coroutine  # type: ignore
def metadata_coroutine(state: ParseState) -> CoroutineGen:  # pragma: no cover
    """Coroutine to build the metadata object from streaming JSON events."""
    builder: ObjectBuilder | None = None
    while True:
        prefix, event, value = yield
        # Handle both "metadata" (root) and "metadata." (nested keys)
        if not (prefix == "metadata" or prefix.startswith(METADATA_PREFIX)):
            continue
        if prefix == "metadata" and event == "start_map":
            builder = ObjectBuilder()
            builder.event(event, value)
            continue
        if builder is None:
            continue
        if prefix == "metadata" and event == "end_map":
            try:
                builder.event(event, value)
                state.metadata = builder.value
            except Exception:
                pass
            builder = None
            continue
        try:
            builder.event(event, value)
        except Exception:
            builder = None
            continue


SCORES_PREFIX = "scores."


@_ijson_coroutine  # type: ignore
def scores_coroutine(state: ParseState) -> CoroutineGen:  # pragma: no cover
    """Coroutine to build the scores object from streaming JSON events."""
    builder: ObjectBuilder | None = None
    while True:
        prefix, event, value = yield
        if not (prefix == "scores" or prefix.startswith(SCORES_PREFIX)):
            continue
        if prefix == "scores" and event == "start_map":
            builder = ObjectBuilder()
            builder.event(event, value)
            continue
        if builder is None:
            continue
        if prefix == "scores" and event == "end_map":
            try:
                builder.event(event, value)
                state.scores = builder.value
            except Exception:
                pass
            builder = None
            continue
        try:
            builder.event(event, value)
        except Exception:
            builder = None
            continue


TARGET_PREFIX = "target."


@_ijson_coroutine  # type: ignore
def target_coroutine(state: ParseState) -> CoroutineGen:  # pragma: no cover
    """Coroutine to capture the target field (scalar string or list of strings)."""
    while True:
        prefix, event, value = yield
        if prefix != "target" and not prefix.startswith(TARGET_PREFIX):
            continue
        if prefix == "target" and event == "string":
            state.target = value
            return
        if prefix == "target" and event == "start_array":
            items: list[str] = []
            while True:
                prefix, event, value = yield
                if prefix == "target" and event == "end_array":
                    state.target = items
                    return
                if prefix == "target.item" and event == "string":
                    items.append(value)


__all__ = [
    "ListProcessingConfig",
    "ParseState",
    "message_item_coroutine",
    "event_item_coroutine",
    "timeline_item_coroutine",
    "message_pool_item_coroutine",
    "call_pool_item_coroutine",
    "attachments_coroutine",
    "metadata_coroutine",
    "scores_coroutine",
    "target_coroutine",
    "SCORES_PREFIX",
    "TARGET_PREFIX",
    "ATTACHMENT_PREFIX",
    "ATTACHMENT_PREFIX_LEN",
    "ATTACHMENTS_PREFIX",
    "MESSAGES_ITEM_PREFIX",
    "EVENTS_ITEM_PREFIX",
    "TIMELINES_ITEM_PREFIX",
    "MESSAGE_POOL_ITEM_PREFIX",
    "CALL_POOL_ITEM_PREFIX",
    "METADATA_PREFIX",
]
