import contextlib
import os
import re
from argparse import ArgumentParser
from tempfile import NamedTemporaryFile
from typing import Any

import polib
from django.apps import apps
from django.conf import settings
from django.core.management.base import BaseCommand, CommandError

from core.management.commands import RootDirectory, TRANSLATABLE_APPS

# Patterns to identify placeholders
PLACEHOLDER_REGEXES = [
    re.compile(r"\{[^}]+"),  # {name}, {type(instance)!s}, etc.
    re.compile(r"%\([^)]+\)[sd]"),  # %(verbose_name)s, %(count)d
]


def extract_placeholders(text: str) -> set[str]:
    """
    Extract all placeholders from the given text.
    """
    phs: list[str] = []
    for rx in PLACEHOLDER_REGEXES:
        phs.extend(rx.findall(text))
    return set(phs)


def load_po_sanitized(path: str) -> polib.POFile:
    """
    Load a .po file via polib, sanitizing on parse errors.
    Raises CommandError if still unparsable.
    """
    with contextlib.suppress(Exception):
        return polib.pofile(path)

    # read raw text
    try:
        with open(path, encoding="utf-8") as f:
            text = f.read()
    except OSError as e:
        raise CommandError(f"{path}: cannot read file ({e})") from e
    # fix fuzzy flags and empty header entries
    text = re.sub(r"^#,(?!\s)", "#, ", text, flags=re.MULTILINE)
    parts = text.split("\n\n", 1)
    header = parts[0]
    rest = parts[1] if len(parts) > 1 else ""
    rest = re.sub(r"^msgid \"\"\s*\nmsgstr \"\"\s*\n?", "", rest, flags=re.MULTILINE)
    sanitized = header + "\n\n" + rest
    tmp = NamedTemporaryFile(mode="w+", delete=False, suffix=".po", encoding="utf-8")  # noqa: SIM115
    try:
        tmp.write(sanitized)
        tmp.flush()
        tmp.close()
        return polib.pofile(tmp.name)
    except Exception as e:
        raise CommandError(f"{path}: syntax error after sanitization ({e})") from e
    finally:
        with contextlib.suppress(OSError):
            os.unlink(tmp.name)


class Command(BaseCommand):
    help = "Scan target-language .po files and report any placeholder mismatches, grouped by app."

    def add_arguments(self, parser: ArgumentParser) -> None:
        parser.add_argument(
            "-l",
            "--language",
            dest="target_languages",
            action="append",
            required=True,
            metavar="LANG",
            help="Locale code(s) to scan, e.g. de-DE, fr-FR",
        )
        parser.add_argument(
            "-a",
            "--app",
            dest="target_apps",
            action="append",
            required=True,
            metavar="APP",
            help="App label(s) to scan, e.g. core, payments",
        )
        parser.add_argument(
            "-p",
            "--path",
            dest="root_path",
            required=False,
            metavar="ROOT_PATH",
            help="Root path prefix to adjust file links",
        )

    def handle(self, *args: list[Any], **options: dict[str, str | list[str]]) -> None:
        langs: list[str] = options.get("target_languages", [])  # type: ignore [assignment]
        if "ALL" in langs:
            langs = list(dict(settings.LANGUAGES).keys())
        apps_to_scan: set[str] = set(options["target_apps"])
        if "ALL" in apps_to_scan:
            apps_to_scan = set(TRANSLATABLE_APPS)
        root_path: str = options.get("root_path") or "/app/"  # type: ignore [assignment]

        configs = list(apps.get_app_configs())
        # noinspection PyTypeChecker
        configs.append(RootDirectory())  # type: ignore [arg-type]

        errors = 0

        for app_conf in configs:
            if app_conf.label not in apps_to_scan:
                continue

            # Collect issues per app across all languages
            app_issues: list[str] = []

            for lang in langs:
                loc = lang.replace("-", "_")
                po_path = os.path.join(app_conf.path, "locale", loc, "LC_MESSAGES", "django.po")
                if not os.path.exists(po_path):
                    continue

                try:
                    po = load_po_sanitized(po_path)
                except CommandError:
                    continue

                # Collect lines for this language
                lang_issues: list[str] = []
                for entry in po:
                    if not entry.msgid or not entry.msgstr or entry.obsolete:
                        continue
                    src_ph = extract_placeholders(entry.msgid)
                    dst_ph = extract_placeholders(entry.msgstr)
                    missing = src_ph - dst_ph
                    extra = dst_ph - src_ph
                    if missing or extra:
                        line_no = entry.linenum or "?"
                        display = po_path.replace("/app/", root_path)
                        if "\\" in root_path:
                            display = display.replace("/", "\\")
                        lang_issues.append(f"    {display}:{line_no}: missing={sorted(missing)} extra={sorted(extra)}")

                if lang_issues:
                    # Header for language with issues
                    app_issues.append(f"  ► {lang}")
                    app_issues.extend(lang_issues)

            # Output per app
            if app_issues:
                self.stdout.write(f"→ App: {app_conf.label}")
                for line in app_issues:
                    errors += 1
                    self.stdout.write(line)
                self.stdout.write("")
            else:
                # No issues in any language for this app
                self.stdout.write(self.style.SUCCESS(f"App {app_conf.label} has no placeholder issues."))
                self.stdout.write("")

        self.stdout.write(self.style.SUCCESS("Done scanning."))
        if errors:
            exit(1)
