"""Audit translator-facing tree files before syncing them."""

from __future__ import annotations

from pathlib import Path

from .._template_transform.markers import GENERATED_BLOCK_PATTERN, generated_block_body
from .document import parse_translation_document
from .manifest import TREE_MANIFEST_PATH, load_tree_manifest
from .models import TranslationTreeAuditIssue, TranslationTreeError
from .placeholders import (
    build_source_placeholder_map,
    contains_raw_jinja_in_translation,
    source_placeholder_counts,
    validate_translation_placeholders,
)
from .source_text import (
    contains_jinja_block_or_comment,
    hard_fragment_sentence_message,
)
from .workspace import validate_expanded_workspace


def audit_translation_tree(
    *,
    tree_dir: Path,
    source_dir: Path,
    source_lang: str = "en",
    target_lang: str = "zh_Hant",
) -> list[TranslationTreeAuditIssue]:
    """Return structural issues that make translation blocks unsafe to edit."""

    tree_dir = Path(tree_dir).resolve()
    source_dir = Path(source_dir).resolve()
    issues: list[TranslationTreeAuditIssue] = []

    try:
        validate_expanded_workspace(source_dir)
    except TranslationTreeError as exc:
        return [
            TranslationTreeAuditIssue(
                code="invalid-expanded-workspace",
                location=str(source_dir),
                message=str(exc),
            )
        ]

    try:
        manifest = load_tree_manifest(tree_dir)
    except TranslationTreeError as exc:
        return [
            TranslationTreeAuditIssue(
                code="invalid-translation-tree",
                location=str(tree_dir),
                message=str(exc),
            )
        ]

    units = manifest.get("units")
    if not isinstance(units, list):
        return [
            TranslationTreeAuditIssue(
                code="invalid-translation-tree",
                location=str(tree_dir / TREE_MANIFEST_PATH),
                message="Manifest `units` must be a list.",
            )
        ]

    for unit in units:
        issues.extend(
            _audit_manifest_unit(
                tree_dir=tree_dir,
                source_dir=source_dir,
                unit=unit,
                source_lang=source_lang,
                target_lang=target_lang,
            )
        )

    return issues


def _audit_manifest_unit(
    *,
    tree_dir: Path,
    source_dir: Path,
    unit: object,
    source_lang: str,
    target_lang: str,
) -> list[TranslationTreeAuditIssue]:
    issues: list[TranslationTreeAuditIssue] = []
    if not isinstance(unit, dict):
        return [
            TranslationTreeAuditIssue(
                code="invalid-manifest-unit",
                location=str(tree_dir / TREE_MANIFEST_PATH),
                message=f"Manifest unit must be an object, got {type(unit).__name__}.",
            )
        ]

    source_file = unit.get("source_file")
    unit_key = unit.get("unit_key")
    document_path_raw = unit.get("document_path")
    if not isinstance(source_file, str) or not isinstance(unit_key, str):
        return [
            TranslationTreeAuditIssue(
                code="invalid-manifest-unit",
                location=str(tree_dir / TREE_MANIFEST_PATH),
                message="Manifest unit is missing a valid source_file or unit_key.",
            )
        ]
    location = source_file if not isinstance(unit_key, str) else f"{source_file} ({unit_key})"

    try:
        source_unit_text = _source_unit_text_from_manifest_unit(
            source_dir=source_dir,
            unit=unit,
        )
    except TranslationTreeError as exc:
        issues.append(
            TranslationTreeAuditIssue(
                code="invalid-source-span",
                location=location,
                message=str(exc),
            )
        )
        source_unit_text = None

    if source_unit_text is not None:
        issues.extend(_audit_source_unit_text(source_unit_text, location=location))

    document_path_issue = _audit_document_path(
        tree_dir=tree_dir,
        document_path_raw=document_path_raw,
        location=location,
    )
    if document_path_issue is not None:
        issues.append(document_path_issue)
        return issues

    assert isinstance(document_path_raw, str)
    try:
        translation_text = parse_translation_document(
            document_path=tree_dir / document_path_raw,
            source_lang=source_lang,
            target_lang=target_lang,
        )
    except TranslationTreeError as exc:
        issues.append(
            TranslationTreeAuditIssue(
                code="invalid-translation-document",
                location=document_path_raw,
                message=str(exc),
            )
        )
        return issues

    issues.extend(
        _audit_translation_text(
            source_file=source_file,
            unit_key=unit_key,
            document_path_raw=document_path_raw,
            source_unit_text=source_unit_text,
            translation_text=translation_text,
        )
    )

    return issues


def _audit_source_unit_text(
    source_unit_text: str,
    *,
    location: str,
) -> list[TranslationTreeAuditIssue]:
    issues: list[TranslationTreeAuditIssue] = []
    if contains_jinja_block_or_comment(source_unit_text):
        issues.append(
            TranslationTreeAuditIssue(
                code="unsafe-source-jinja-block",
                location=location,
                message=(
                    "Source unit contains raw Jinja block/comment syntax. "
                    "Expand/export must split this before translators edit it."
                ),
            )
        )

    hard_fragment_message = hard_fragment_sentence_message(source_unit_text)
    if hard_fragment_message is not None:
        issues.append(
            TranslationTreeAuditIssue(
                code="hard-to-translate-source-fragment",
                location=location,
                message=hard_fragment_message,
            )
        )

    ambiguous_names = _ambiguous_source_placeholder_names(source_unit_text)
    if ambiguous_names:
        issues.append(
            TranslationTreeAuditIssue(
                code="ambiguous-source-placeholder",
                location=location,
                message=(
                    "Source unit has placeholder names that cannot be mapped back "
                    f"unambiguously: {', '.join('{' + name + '}' for name in ambiguous_names)}"
                ),
            )
        )
    return issues


def _ambiguous_source_placeholder_names(source_unit_text: str) -> list[str]:
    placeholder_map = build_source_placeholder_map(source_unit_text)
    source_placeholder_names = set(source_placeholder_counts(source_unit_text))
    return sorted(source_placeholder_names - set(placeholder_map))


def _audit_document_path(
    *,
    tree_dir: Path,
    document_path_raw: object,
    location: str,
) -> TranslationTreeAuditIssue | None:
    if not isinstance(document_path_raw, str):
        return TranslationTreeAuditIssue(
            code="invalid-manifest-unit",
            location=location,
            message="Manifest unit is missing a valid document_path.",
        )

    document_path = tree_dir / document_path_raw
    if not document_path.is_file():
        return TranslationTreeAuditIssue(
            code="missing-translation-document",
            location=document_path_raw,
            message="Translation document is missing. Re-run export to restore it.",
        )
    return None


def _audit_translation_text(
    *,
    source_file: str,
    unit_key: str,
    document_path_raw: str,
    source_unit_text: str | None,
    translation_text: str,
) -> list[TranslationTreeAuditIssue]:
    issues: list[TranslationTreeAuditIssue] = []
    has_raw_jinja_translation = contains_raw_jinja_in_translation(translation_text)
    if has_raw_jinja_translation:
        issues.append(
            TranslationTreeAuditIssue(
                code="raw-jinja-in-translation",
                location=document_path_raw,
                message=(
                    "Translation block contains raw Jinja syntax. Use translator "
                    "placeholders such as `{name}` instead of `{% ... %}` or `{{ ... }}`."
                ),
            )
        )

    if source_unit_text is None or not translation_text.strip() or has_raw_jinja_translation:
        return issues

    try:
        validate_translation_placeholders(
            source_file=source_file,
            unit_key=unit_key,
            translation_document_path=document_path_raw,
            source_text=source_unit_text,
            translation_text=translation_text,
        )
    except TranslationTreeError as exc:
        issues.append(
            TranslationTreeAuditIssue(
                code="invalid-translation-placeholders",
                location=document_path_raw,
                message=str(exc),
            )
        )
    return issues


def _source_unit_text_from_manifest_unit(*, source_dir: Path, unit: dict) -> str:
    source_file = unit.get("source_file")
    wrapper_order = unit.get("wrapper_order")
    unit_start = unit.get("unit_start")
    unit_end = unit.get("unit_end")
    if (
        not isinstance(source_file, str)
        or not isinstance(wrapper_order, int)
        or not isinstance(unit_start, int)
        or not isinstance(unit_end, int)
    ):
        raise TranslationTreeError("Manifest unit has invalid source span metadata.")

    source_path = source_dir / source_file
    if not source_path.is_file():
        raise TranslationTreeError(f"Source file is missing: {source_path}")
    source_text = source_path.read_text(encoding="utf-8")
    wrappers = list(GENERATED_BLOCK_PATTERN.finditer(source_text))
    if wrapper_order < 1 or wrapper_order > len(wrappers):
        raise TranslationTreeError(
            f"Wrapper order {wrapper_order} does not exist in {source_file}."
        )
    wrapper_body = generated_block_body(wrappers[wrapper_order - 1])
    if unit_start < 0 or unit_end > len(wrapper_body) or unit_start >= unit_end:
        raise TranslationTreeError(
            f"Unit span {unit_start}:{unit_end} is invalid for {source_file}."
        )
    return wrapper_body[unit_start:unit_end]
