# Standard Library
import html
import os
import re
import time
import uuid
from datetime import datetime
from pathlib import Path

# Django
from django.conf import settings

# Third party
import polib

TRANSLATION_IGNORE_PATTERNS = getattr(
    settings,
    "TRANSLATION_IGNORE_PATTERNS",
    [
        r"%\(\w+\)[sd]",  # Must be before %(w+) to avoid breaking %(name)s
        r"%\(\w+\)",
        r"%[dsf]",
        r"(\{\{[^{}]+\}\}|\{[^{}]+\})",
        r"&[a-zA-Z]+;",  # Entités HTML
    ],
)
TAG_HANDLING = "xml"
IGNORE_TAGS_TEXT = getattr(settings, "IGNORE_TAGS_TEXT", "ddlit")
IGNORE_TAGS = getattr(
    settings, "IGNORE_TAGS", (f"<{IGNORE_TAGS_TEXT}>", f"</{IGNORE_TAGS_TEXT}>")
)

BACKUP_FILE_NAME = getattr(settings, "BACKUP_FILE_NAME", "django_deepl_backup")
PO_FILE_EXTENSION = getattr(settings, "PO_FILE_EXTENSION", ".po")
PO_FILE_NAME = getattr(settings, "PO_FILE_NAME", "django")
DEEPL_API_KEY = getattr(settings, "DEEPL_API_KEY", None)

# Placeholder pattern for brace-format (used in attributes to avoid invalid XML)
BRACE_PLACEHOLDER_PATTERN = re.compile(r"\{[^{}]+\}")

# HTML named entities -> numeric (XML-compatible). DeepL's XML parser fails on &nbsp; etc.
_HTML_ENTITY_TO_NUMERIC = {
    "&nbsp;": "&#160;",
    "&eacute;": "&#233;",
    "&egrave;": "&#232;",
    "&agrave;": "&#224;",
    "&ucirc;": "&#251;",
    "&ecirc;": "&#234;",
    "&icirc;": "&#238;",
    "&ocirc;": "&#244;",
    "&acirc;": "&#226;",
    "&ccedil;": "&#231;",
    "&ugrave;": "&#249;",
    "&copy;": "&#169;",
    "&reg;": "&#174;",
    "&trade;": "&#8482;",
    "&mdash;": "&#8212;",
    "&ndash;": "&#8211;",
    "&hellip;": "&#8230;",
    "&lsquo;": "&#8216;",
    "&rsquo;": "&#8217;",
    "&ldquo;": "&#8220;",
    "&rdquo;": "&#8221;",
}


_HTML_ENTITY_RE = re.compile(r"&([a-zA-Z][a-zA-Z0-9]*);")


# HTML void tags that must be self-closed for XML (e.g. <br> -> <br />)
_HTML_VOID_TAGS = ("br", "hr", "img", "input", "meta", "link", "area", "base", "col")


def _normalize_for_xml(text):
    """
    Normalize text for DeepL's XML parser.
    - Replace newlines with spaces (newlines inside HTML attributes break XML)
    - Close HTML void tags: <br> -> <br /> (XML requires closed tags)
    """
    if not text:
        return text
    text = text.replace("\n", " ")
    for tag in _HTML_VOID_TAGS:
        # Match <tag> or <tag attr="x">; ensure XML-style close: <br> -> <br />
        pattern = re.compile(rf"<{tag}(?:\s[^>]*)?>", re.IGNORECASE)

        def make_repl():
            def repl(m):
                s = m.group(0)
                if s.rstrip().endswith("/>"):
                    return s
                return s[:-1] + " />"

            return repl

        text = pattern.sub(make_repl(), text)
    return text


def _html_entities_to_numeric(text):
    """Replace HTML named entities with numeric refs for XML compatibility."""
    if not text:
        return text
    result = text
    for entity, numeric in _HTML_ENTITY_TO_NUMERIC.items():
        result = result.replace(entity, numeric)

    def repl(m):
        name = m.group(1).lower()
        codepoint = html.entities.name2codepoint.get(name)
        if codepoint is not None:
            return f"&#{codepoint};"
        return m.group(0)

    result = _HTML_ENTITY_RE.sub(repl, result)
    return result


# Sentinel for placeholders inside HTML attributes (no angle brackets)
DEEPL_ATTR_PLACEHOLDER_PREFIX = "\u2063"  # Invisible separator
DEEPL_ATTR_PLACEHOLDER_SUFFIX = "\u2063"


def _protect_placeholders_in_attributes(text):
    """
    Replace placeholders inside HTML/XML attribute values with a safe marker.
    Using <ddlit> inside attributes creates invalid XML (unescaped <).
    Returns (modified_text, restore_map) for reversing after translation.
    """
    restore_map = {}

    # Match attribute values: ="..." or ='...'
    def replacer(match):
        quote = match.group(1)
        attr_content = match.group(2)
        if not BRACE_PLACEHOLDER_PATTERN.search(attr_content):
            return match.group(0)
        new_content = BRACE_PLACEHOLDER_PATTERN.sub(
            lambda m: restore_map.setdefault(
                m.group(0),
                f"{DEEPL_ATTR_PLACEHOLDER_PREFIX}{uuid.uuid4().hex}"
                f"{DEEPL_ATTR_PLACEHOLDER_SUFFIX}",
            ),
            attr_content,
        )
        return f"={quote}{new_content}{quote}"

    text = re.sub(r'=(["\'])([^"\']*?)\1', replacer, text)
    return text, restore_map


def _restore_placeholders_in_attributes(text, restore_map):
    """Reverse the placeholder replacement."""
    if not restore_map:
        return text
    reverse_map = {v: k for k, v in restore_map.items()}
    pattern = re.compile(
        f"{re.escape(DEEPL_ATTR_PLACEHOLDER_PREFIX)}[a-f0-9]+"
        f"{re.escape(DEEPL_ATTR_PLACEHOLDER_SUFFIX)}"
    )
    return pattern.sub(lambda m: reverse_map.get(m.group(0), m.group(0)), text)


# Pattern: %(name)s - to detect placeholders in source
_PYTHON_FORMAT_PLACEHOLDER = re.compile(r"%\((\w+)\)[sd]")
# Placeholder names that are counts/numbers - fix %(name)'s -> %(name)s
# (Keep possessive for names like %(audit)'s -> "the audit's report")
_COUNT_PLACEHOLDER_NAMES = frozenset(
    ("nombre", "count", "compteur", "n", "num", "number")
)


def _fix_placeholder_possessive(translated_text, source_text):
    """
    Fix DeepL adding possessive 's after count placeholders.
    When source has %(nombre)s and translation has %(nombre)'s,
    replace with %(nombre)s for "1 response" vs "1's response".
    Only for count-like placeholders (nombre, count, etc.), not names (audit).
    """
    if not source_text or not translated_text:
        return translated_text
    if isinstance(translated_text, dict):
        return {
            k: _fix_placeholder_possessive(v, source_text)
            for k, v in translated_text.items()
        }
    result = translated_text
    for match in _PYTHON_FORMAT_PLACEHOLDER.finditer(source_text):
        name = match.group(1)
        if name.lower() not in _COUNT_PLACEHOLDER_NAMES:
            continue
        ph = match.group(0)
        wrong = ph + "'s"
        if wrong in result:
            result = result.replace(wrong, ph)
    return result


def extract_timestamp_from_filename(file_name):
    backup_file_pattern = re.compile(
        f"^{re.escape(BACKUP_FILE_NAME)}_(\\d+){re.escape(PO_FILE_EXTENSION)}$"
    )
    match = backup_file_pattern.fullmatch(file_name)
    if match:
        return match.group(1)
    return None


def is_valid_timestamp(timestamp_str):
    try:
        int(timestamp_str)
        return True
    except ValueError:
        return False


def is_today_timestamp(timestamp_str):
    if is_valid_timestamp(timestamp_str):
        timestamp = int(timestamp_str)
        file_date = datetime.fromtimestamp(timestamp).date()
        today_date = datetime.today().date()
        return file_date == today_date
    return False


def generate_po_backup_filename():
    backup_timestamp = int(time.time())
    return f"{BACKUP_FILE_NAME}_{backup_timestamp}{PO_FILE_EXTENSION}"


def get_apps_name():
    """Return list of app names that have locale folders, or LOCALE_PATHS entries."""
    app_list = []
    base_dir = settings.BASE_DIR

    # Support LOCALE_PATHS (e.g. for projects with src/backend/locale, src/libs/locale)
    locale_paths = getattr(settings, "LOCALE_PATHS", [])
    for i, locale_path in enumerate(locale_paths):
        if Path(locale_path).exists() and os.path.isdir(locale_path):
            app_list.append(f"locale_path_{i}")

    # Legacy: scan base_dir and INSTALLED_APPS for locale folders
    try:
        base_dir_contents = os.listdir(base_dir)
    except (PermissionError, OSError):
        base_dir_contents = []
    dir_list = list(set(base_dir_contents) | set(settings.INSTALLED_APPS))
    for directory in dir_list:
        if directory == "locale" and os.path.isdir(os.path.join(base_dir, directory)):
            app_list.append("project_base_directory")
            continue
        app_path = os.path.join(base_dir, directory)
        if os.path.isdir(app_path):
            try:
                app_contents = os.listdir(app_path)
            except (PermissionError, OSError):
                continue
            locale_path = os.path.join(app_path, "locale")
            if "locale" in app_contents and os.path.isdir(locale_path):
                if directory not in app_list:
                    app_list.append(directory)
    return app_list


def get_app_path(app_name):
    """Return the path for an app (directory or LOCALE_PATHS entry)."""
    if app_name.startswith("locale_path_"):
        idx = int(app_name.split("_")[-1])
        return settings.LOCALE_PATHS[idx]
    if app_name == "project_base_directory":
        return settings.BASE_DIR
    return os.path.join(settings.BASE_DIR, app_name)


def get_locale_path(app_name):
    """Return the locale path for an app."""
    if app_name.startswith("locale_path_"):
        return get_app_path(app_name)
    return os.path.join(get_app_path(app_name), "locale")


def get_all_languages():
    """Return all language codes found in locale folders."""
    languages = []
    apps = get_apps_name()

    for app in apps:
        locale_path = get_locale_path(app)
        if Path(locale_path).exists() and os.path.isdir(locale_path):
            for lang in os.listdir(locale_path):
                lang_path = os.path.join(locale_path, lang)
                if os.path.isdir(lang_path) and lang not in languages:
                    languages.append(lang)

    for lang_code in settings.LANGUAGES:
        if lang_code[0] not in languages:
            languages.append(lang_code[0])
    return languages


def check_translation_status(self, *args, **kwargs):
    """Check translation status for apps and return list of files to translate."""
    app_to_check = kwargs.get("app")
    language_to_check = kwargs.get("language")
    base_language = settings.LANGUAGE_CODE
    po_files_to_translate = []

    self.stdout.write(
        self.style.SUCCESS("\nChecking translations for Django apps...\n")
    )

    for app in app_to_check:
        try:
            app_path = get_app_path(app)
            locale_path = get_locale_path(app)

            if not os.path.isdir(app_path):
                continue
            if not Path(locale_path).exists():
                self.stdout.write(f"\nApp: {app}")
                self.stdout.write(
                    f" - Exists in project directory: {'✅' if os.path.isdir(app_path) else '❌'}"
                )
                self.stdout.write(
                    self.style.ERROR(" - No translation folder (locale) found!")
                )
                continue

            missing_languages = []
            all_languages_present = True
            for lang in language_to_check:
                po_file_path = os.path.join(
                    locale_path,
                    lang,
                    "LC_MESSAGES",
                    f"{PO_FILE_NAME}{PO_FILE_EXTENSION}",
                )
                if not Path(po_file_path).exists():
                    all_languages_present = False
                    missing_languages.append(lang)

            self.stdout.write(f"\nApp: {app}")
            self.stdout.write(
                f" - Has translation folder: {'✅' if Path(locale_path).exists() else '❌'}"
            )
            if all_languages_present:
                self.stdout.write(
                    self.style.SUCCESS(" - All required languages are available ✅")
                )
            else:
                self.stdout.write(
                    self.style.WARNING(
                        f" - Folder not found for: {', '.join(missing_languages)}"
                    )
                )

            for lang in language_to_check:
                po_file_path = os.path.join(
                    locale_path,
                    lang,
                    "LC_MESSAGES",
                    f"{PO_FILE_NAME}{PO_FILE_EXTENSION}",
                )
                if Path(po_file_path).exists():
                    file_translation_details = {
                        "app": app,
                        "translate_from": base_language,
                        "translate_to": lang,
                        "file_path": po_file_path,
                    }
                    po_files_to_translate.append(file_translation_details)
                    try:
                        po_file = polib.pofile(po_file_path)
                        total_entries = len(
                            [e for e in po_file if e.msgid.strip() != ""]
                        )
                        translated_entries = len([e for e in po_file if e.translated()])
                        completion_rate = (
                            (translated_entries / total_entries * 100)
                            if total_entries > 0
                            else 0
                        )
                        self.stdout.write(f"   • {lang}: {int(completion_rate)}%")
                    except Exception as e:
                        self.stdout.write(
                            self.style.ERROR(
                                f"Error reading PO file for {lang} in app {app}: {e}"
                            )
                        )
        except Exception as e:
            self.stdout.write(self.style.ERROR(f"Error checking app {app}: {e}"))
            exit(0)

    self.stdout.write(self.style.SUCCESS("\nCheck completed!"))
    return po_files_to_translate
