"""Language-specific polish for synced translated template output."""

from __future__ import annotations

import re
from collections.abc import Callable
from dataclasses import dataclass
from pathlib import Path

_CJK = "\u3400-\u9fff"
_CJK_OR_JINJA_END_CLASS = _CJK + "）】》』」}"
_FULLWIDTH_PUNCTUATION_BEFORE_CJK = "。：；，、）】》』」"
_FULLWIDTH_BOUNDARY_BEFORE_SILENT_JINJA_CJK = "。：；）】》』」"
_VISIBLE_BOUNDARY_BEFORE_SILENT_JINJA_CJK = _CJK_OR_JINJA_END_CLASS + "。：；>"
_FAIRSHARING_MACRO_LINE_PATTERN = re.compile(
    r"(?m)^([ \t]*):\s*(\{\{\s*macros\.integrationFairSharing\([^}\n]+\)\s*\}\})\."
)
_LOOP_COMMA_PERIOD_PATTERN = re.compile(r'\{\{\s*", "\s+if\s+not\s+loop\.last\s+else\s+"\."\s*\}\}')
_JOIN_COMMA_PATTERN = re.compile(r'\|\s*join\(", "\)')
_SENTENCE_LIST_JOIN_SPACE_PATTERN = re.compile(
    r"""(?P<name>\b(?:[A-Za-z_][A-Za-z0-9_]*)?sentences)\|\s*join\((?P<quote>["']) (?P=quote)\)""",
    re.IGNORECASE,
)
_INLINE_COLON_PREFIX_PATTERN = re.compile(r'\{\{\s*": "\s*~')
_INLINE_PERIOD_FALLBACK_PATTERN = re.compile(r'\s+else\s+"\."\s*\}\}')
_INLINE_TAG_BEFORE_FULLWIDTH_PAREN_PATTERN = re.compile(r"(</(?:em|span|strong)>)\s+（")
_JINJA_STRING_TRAILING_FULLWIDTH_GAP_PATTERN = re.compile(
    rf"(?<=[{_FULLWIDTH_PUNCTUATION_BEFORE_CJK}])[ \t]+(?=['\"])",
)
_DOT_FILTER_PLACEHOLDERS_WITH_TRANSLATED_PUNCTUATION = (
    "swPIDReply",
    "publishedDataHowLongFixed",
    "publishedDataIdentifierSpecifyReply",
    "embargoPeriod",
)
_COST_DESCRIPTION_DOT_PATTERN = re.compile(
    r'\{\{\s*" - "\s*\+\s*projectCostItemDescriptionReply\|dot\s*\}\}'
)
_COST_DESCRIPTION_DOT_REPLACEMENT = (
    "{%- set __tr_dot_value = projectCostItemDescriptionReply|trim -%}"
    '{%- if __tr_dot_value -%}{{ " - " + __tr_dot_value }}'
    '{{ "。" if __tr_dot_value[-1] not in ".。!！?？" else "" }}{%- endif -%}'
)
_REMAINING_DOT_FILTER_PATTERN = re.compile(r"\|dot\b")
_MACHINE_CONTROLLED_REGION_PATTERN = re.compile(
    r"(?:\{\{.*?\}\}|\{%.*?%\}|\{#.*?#\}|<[^>]*>)",
    re.DOTALL,
)


@dataclass(frozen=True)
class SilentSpacingCollapse:
    """Silent Jinja tags to keep while removing following visible whitespace."""

    silent_tags: list[str]
    next_index: int
    remove_spacing: bool


def _is_cjk(char: str) -> bool:
    return "\u3400" <= char <= "\u9fff"


def _read_silent_jinja_tag(text: str, start: int) -> tuple[str, int] | None:
    """Read a non-outputting Jinja tag at `start`, if one is present."""

    if text.startswith("{%", start):
        end_marker = "%}"
    elif text.startswith("{#", start):
        end_marker = "#}"
    else:
        return None

    end = text.find(end_marker, start + 2)
    if end == -1:
        return None
    end += len(end_marker)
    return text[start:end], end


def _collapse_fullwidth_spacing_before_cjk(text: str) -> str:
    """Remove visible whitespace between Chinese punctuation and Chinese text.

    Jinja control/comment tags do not emit visible text, but the surrounding
    template newlines do. Keep those tags while stripping only the whitespace
    that would become an unwanted half-width gap in rendered zh-Hant output.
    """

    result: list[str] = []
    index = 0
    while index < len(text):
        char = text[index]
        result.append(char)
        index += 1

        if char not in _FULLWIDTH_PUNCTUATION_BEFORE_CJK:
            continue

        lookahead = index
        silent_tags: list[str] = []
        allow_silent_tags = char in _FULLWIDTH_BOUNDARY_BEFORE_SILENT_JINJA_CJK
        while True:
            while lookahead < len(text) and text[lookahead] in " \t\r\n":
                lookahead += 1

            if not allow_silent_tags:
                break

            silent_tag = _read_silent_jinja_tag(text, lookahead)
            if silent_tag is None:
                break

            tag_text, lookahead = silent_tag
            silent_tags.append(tag_text)

        if lookahead > index and lookahead < len(text) and _is_cjk(text[lookahead]):
            result.extend(silent_tags)
            index = lookahead

    return "".join(result)


def _collapse_silent_jinja_leading_spacing_before_cjk(text: str) -> str:
    """Remove indentation emitted after silent Jinja branches before zh-Hant text."""

    result: list[str] = []
    index = 0
    last_visible_char = ""
    while index < len(text):
        silent_tag = _read_silent_jinja_tag(text, index)
        if silent_tag is not None:
            tag_text, tag_end = silent_tag
            collapse = _silent_spacing_collapse_after_tag(
                text=text,
                tag_text=tag_text,
                tag_end=tag_end,
                last_visible_char=last_visible_char,
            )
            if collapse is not None:
                if not collapse.remove_spacing:
                    result.append(text[index : collapse.next_index])
                    index = collapse.next_index
                    continue
                while result and result[-1].isspace():
                    result.pop()
                result.extend(collapse.silent_tags)
                index = collapse.next_index
                continue

            result.append(tag_text)
            index = tag_end
            continue

        char = text[index]
        result.append(char)
        if not char.isspace():
            last_visible_char = char
        index += 1

    return "".join(result)


def _silent_spacing_collapse_after_tag(
    *,
    text: str,
    tag_text: str,
    tag_end: int,
    last_visible_char: str,
) -> SilentSpacingCollapse | None:
    if last_visible_char not in _VISIBLE_BOUNDARY_BEFORE_SILENT_JINJA_CJK:
        return None

    lookahead = tag_end
    silent_tags = [tag_text]
    while True:
        while lookahead < len(text) and text[lookahead] in " \t\r\n":
            lookahead += 1

        next_silent_tag = _read_silent_jinja_tag(text, lookahead)
        if next_silent_tag is None:
            break

        next_tag_text, lookahead = next_silent_tag
        silent_tags.append(next_tag_text)

    remove_spacing = (
        lookahead > tag_end
        and lookahead < len(text)
        and (_is_cjk(text[lookahead]) or text[lookahead] == "（")
    )
    return SilentSpacingCollapse(
        silent_tags=silent_tags,
        next_index=lookahead,
        remove_spacing=remove_spacing,
    )


def polish_translated_output_dir(*, output_dir: Path, target_lang: str) -> None:
    """Apply safe, text-level polish after translations are synced.

    Translation units intentionally avoid owning surrounding Jinja punctuation.
    For zh-Hant output, a few punctuation marks from the English template remain
    outside translation blocks, so normalize them at the final output boundary.
    """

    if target_lang != "zh_Hant":
        return

    for path in (output_dir / "src").rglob("*.j2"):
        path.write_text(
            polish_zh_hant_template_text(path.read_text(encoding="utf-8")),
            encoding="utf-8",
        )


def polish_zh_hant_template_text(text: str) -> str:
    """Normalize punctuation patterns that are outside translation units."""

    text = _normalize_dot_filters(text)
    text = _normalize_template_punctuation(text)
    text = _collapse_silent_jinja_leading_spacing_before_cjk(text)
    text = _collapse_fullwidth_spacing_before_cjk(text)
    text = re.sub(rf"(?<=[{_CJK}])、\s+", "、", text)
    text = re.sub(rf"(?<=[{_CJK}]),\s+(?=[{_CJK}])", "、", text)
    return text


def _normalize_dot_filters(text: str) -> str:
    for placeholder in _DOT_FILTER_PLACEHOLDERS_WITH_TRANSLATED_PUNCTUATION:
        text = re.sub(
            r"\{\{\s*" + re.escape(placeholder) + r"\|dot\s*\}\}",
            "{{ " + placeholder + "|trim }}",
            text,
        )
    text = _COST_DESCRIPTION_DOT_PATTERN.sub(_COST_DESCRIPTION_DOT_REPLACEMENT, text)
    text = _REMAINING_DOT_FILTER_PATTERN.sub("|trim", text)
    return text


def _normalize_template_punctuation(text: str) -> str:
    text = _FAIRSHARING_MACRO_LINE_PATTERN.sub(r"\1：\2。", text)
    text = _LOOP_COMMA_PERIOD_PATTERN.sub('{{ "、" if not loop.last else "。" }}', text)
    text = _JOIN_COMMA_PATTERN.sub('|join("、")', text)
    text = _SENTENCE_LIST_JOIN_SPACE_PATTERN.sub(r'\g<name>|join("")', text)
    text = _INLINE_COLON_PREFIX_PATTERN.sub('{{ "：" ~', text)
    text = _INLINE_PERIOD_FALLBACK_PATTERN.sub(' else "。" }}', text)
    text = text.replace(
        "{% if not loop.last %}, {% endif %}",
        "{% if not loop.last %}、{% endif %}",
    )
    text = re.sub(r"(\{%-\s*else\s*-%})\.", r"\1。", text)
    text = _JINJA_STRING_TRAILING_FULLWIDTH_GAP_PATTERN.sub("", text)
    text = _INLINE_TAG_BEFORE_FULLWIDTH_PAREN_PATTERN.sub(r"\1（", text)
    return _transform_visible_template_text(text, _normalize_visible_punctuation)


def _transform_visible_template_text(text: str, transform: Callable[[str], str]) -> str:
    """Transform rendered prose while preserving Jinja and HTML tag contents."""

    result: list[str] = []
    previous_end = 0
    for match in _MACHINE_CONTROLLED_REGION_PATTERN.finditer(text):
        visible_text = text[previous_end : match.start()]
        if previous_end:
            # Preserve the boundary context (notably a closing Jinja brace)
            # without passing the protected region itself to the transform.
            visible_text = transform(text[previous_end - 1] + visible_text)[1:]
        else:
            visible_text = transform(visible_text)
        result.append(visible_text)
        result.append(match.group())
        previous_end = match.end()
    visible_text = text[previous_end:]
    if previous_end:
        visible_text = transform(text[previous_end - 1] + visible_text)[1:]
    else:
        visible_text = transform(visible_text)
    result.append(visible_text)
    return "".join(result)


def _normalize_visible_punctuation(text: str) -> str:
    text = re.sub(rf"(?<=[{_CJK_OR_JINJA_END_CLASS}])\s*:\s*", "：", text)
    text = re.sub(rf"(?<=[{_CJK_OR_JINJA_END_CLASS}])\s+（", "（", text)
    text = re.sub(rf"(?<=[{_CJK_OR_JINJA_END_CLASS}])\.", "。", text)
    return text.replace("。.", "。")
