"""Replay edit events to reconstruct document state."""

from __future__ import annotations

import sys
from typing import Any

from ..timeutil import parse_timestamp
from .document import filter_events_by_document_with_rename_handling
from .load import filter_edit_events


class SuspiciousOffsetEncodingError(ValueError):
    """Raised when recorded offsets do not match the normalized replay state."""


def _normalize_newlines(text: str) -> str:
    """Normalize CRLF to LF for stable replay and diff behavior."""
    return text.replace("\r\n", "\n")


def _ordered_edit_events(events: tuple[dict[str, Any], ...]) -> list[dict[str, Any]]:
    decorated: list[tuple[int, object, dict[str, Any]]] = []
    for index, event in enumerate(events):
        timestamp = event.get("timestamp")
        if timestamp:
            try:
                decorated.append((0, parse_timestamp(str(timestamp)), event))
                continue
            except ValueError:
                pass
        decorated.append((1, index, event))

    decorated.sort(key=lambda item: (item[0], item[1]))
    return [event for _, _, event in decorated]


def _utf16_units_to_index(text: str, units: int) -> int:
    if units <= 0:
        return 0

    consumed = 0
    index = 0
    for char in text:
        if consumed >= units:
            break
        consumed += 2 if ord(char) > 0xFFFF else 1
        index += 1
    return index


def _crlf_units_to_index(text: str, units: int) -> int:
    if units <= 0:
        return 0

    consumed = 0
    index = 0
    for char in text:
        if consumed >= units:
            break
        consumed += 2 if char == "\n" else 1
        index += 1
    return index


def _uses_crlf_offsets(events: tuple[dict[str, Any], ...]) -> bool:
    return any(
        "\r\n" in str(event.get("oldFragment", ""))
        or "\r\n" in str(event.get("newFragment", ""))
        for event in events
    )


def _raise_for_unknown_offset_encoding(
    document: str,
    offset: int,
    *,
    crlf_mode: bool,
) -> None:
    if crlf_mode or offset <= len(document) or "\n" not in document:
        return

    raise SuspiciousOffsetEncodingError(
        "Encountered an edit offset beyond the normalized document length, "
        "but the recording does not include any CRLF fragments to confirm CRLF "
        "offset mode. This recording may use an unsupported offset encoding. "
        "Please investigate this file."
    )


def _resolve_offset(document: str, old_fragment: str, offset: int, window: int) -> int:
    if old_fragment == "":
        return max(0, min(offset, len(document)))

    if 0 <= offset <= len(document) and document[offset : offset + len(old_fragment)] == old_fragment:
        return offset

    start = max(0, offset - window)
    end = min(len(document), offset + window + len(old_fragment))

    best_match: tuple[int, int] | None = None
    search_at = start
    while True:
        found = document.find(old_fragment, search_at, end)
        if found == -1:
            break
        distance = abs(found - offset)
        candidate = (distance, found)
        if best_match is None or candidate < best_match:
            best_match = candidate
        search_at = found + 1

    if best_match is None:
        raise ValueError(
            f"Old fragment not found near offset {offset}.\n"
            f"old={old_fragment!r}\nnew fragment length={len(old_fragment)}"
        )

    return best_match[1]


def _apply_edit(
    document: str,
    *,
    old_fragment: str,
    new_fragment: str,
    offset: int,
    window: int,
    utf16_mode: bool,
    crlf_mode: bool,
) -> str:
    _raise_for_unknown_offset_encoding(document, offset, crlf_mode=crlf_mode)
    if utf16_mode:
        text_offset = _utf16_units_to_index(document, offset)
    elif crlf_mode:
        text_offset = _crlf_units_to_index(document, offset)
    else:
        text_offset = offset
    resolved_offset = _resolve_offset(document, old_fragment, text_offset, window)
    return (
        document[:resolved_offset]
        + new_fragment
        + document[resolved_offset + len(old_fragment) :]
    )


def reconstruct_file_from_events(
    events: tuple[dict[str, Any], ...],
    template: str,
    document_path: str | None = None,
    *,
    utf16_mode: bool = False,
    window: int = 200,
    normalize_newlines: bool = True,
    skip_unreplayable: bool = True,
) -> str:
    """Replay edit events to reconstruct the final document state."""
    edit_events = filter_edit_events(events)
    if not edit_events:
        return _normalize_newlines(template) if normalize_newlines else template

    target_document = document_path
    if target_document is None:
        recorded_docs = {
            str(event["document"])
            for event in edit_events
            if event.get("document") is not None
        }
        if len(recorded_docs) == 1:
            target_document = next(iter(recorded_docs))
        else:
            raise ValueError(
                "Ambiguous target document: provide document_path explicitly."
            )

    doc_events = tuple(
        filter_events_by_document_with_rename_handling(edit_events, target_document)
    )
    ordered_events = _ordered_edit_events(doc_events)
    if not ordered_events:
        return _normalize_newlines(template) if normalize_newlines else template

    document = _normalize_newlines(template) if normalize_newlines else template
    crlf_mode = normalize_newlines and _uses_crlf_offsets(tuple(ordered_events))
    skipped = 0
    conflicting_snapshots = 0

    for event_index, event in enumerate(ordered_events):
        old_fragment = str(event.get("oldFragment", ""))
        new_fragment = str(event.get("newFragment", ""))
        if normalize_newlines:
            old_fragment = _normalize_newlines(old_fragment)
            new_fragment = _normalize_newlines(new_fragment)

        try:
            offset = int(event.get("offset", 0) or 0)
        except (TypeError, ValueError):
            offset = 0

        if old_fragment == new_fragment and offset == 0:
            if event_index > 0 and old_fragment != document:
                conflicting_snapshots += 1
                print(
                    "Warning: "
                    f"Conflicting snapshot event #{event_index} "
                    f"(timestamp: {event.get('timestamp', 'unknown')})",
                    file=sys.stderr,
                )
            document = old_fragment
            continue

        if old_fragment == new_fragment:
            continue

        try:
            document = _apply_edit(
                document,
                old_fragment=old_fragment,
                new_fragment=new_fragment,
                offset=offset,
                window=window,
                utf16_mode=utf16_mode,
                crlf_mode=crlf_mode,
            )
        except ValueError as exc:
            if isinstance(exc, SuspiciousOffsetEncodingError):
                raise
            if not skip_unreplayable:
                raise

            skipped += 1
            print(
                "Warning: "
                f"Skipping event #{event_index} "
                f"(timestamp: {event.get('timestamp', 'unknown')}): "
                f"{exc} - document offset may have drifted",
                file=sys.stderr,
            )

    if skipped:
        print(
            f"Warning: Skipped {skipped} event(s) due to offset drift",
            file=sys.stderr,
        )
    if conflicting_snapshots:
        print(
            f"Warning: Saw {conflicting_snapshots} conflicting snapshot event(s)",
            file=sys.stderr,
        )

    return document
