# Standard Library
import re
import shutil
import time
from datetime import UTC, datetime

# Django
from django.conf import settings
from django.core.management.base import BaseCommand

# Third party
import deepl
import polib

# Project
# Local
from django_deepl_po.utils import (
    DEEPL_API_KEY,
    IGNORE_TAGS,
    IGNORE_TAGS_TEXT,
    PO_FILE_EXTENSION,
    PO_FILE_NAME,
    TAG_HANDLING,
    TRANSLATION_IGNORE_PATTERNS,
    _fix_placeholder_possessive,
    _html_entities_to_numeric,
    _normalize_for_xml,
    _protect_placeholders_in_attributes,
    _restore_placeholders_in_attributes,
    check_translation_status,
    generate_po_backup_filename,
    get_all_languages,
    get_apps_name,
)


class Command(BaseCommand):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.apps_name = get_apps_name()
        self.deepl_client = None
        self.available_variant = []

    help = f"Automatically translates msgid strings in {PO_FILE_EXTENSION} files into the target language by filling in msgstr values."
    suppressed_base_arguments = [
        "--verbosity",
        "--settings",
        "--pythonpath",
        "--traceback",
        "--no-color",
        "--force-color",
        "--skip-checks",
        "--version",
    ]

    def add_arguments(self, parser):
        parser.add_argument(
            "--app",
            choices=self.apps_name,
            default=self.apps_name,
            nargs="*",
            help="Specify which app(s) to translate (optional). If not provided, all apps will be processed.",
        )
        parser.add_argument(
            "--language",
            choices=get_all_languages(),
            default=get_all_languages(),
            nargs="*",
            help=f"Specify which language(s) {PO_FILE_EXTENSION} file to translate (optional). If not provided, all languages will be processed.",
        )
        parser.add_argument(
            "--overwrite",
            action="store_true",
            default=False,
            help="If set, translates even already translated msgstr values (default: False).",
        )
        parser.add_argument(
            "--split_sentences",
            choices=["1", "0", "nonewlines"],
            default="nonewlines",
            help="Specify how text should be split into sentences. Options: '1' split using newlines and punctuation, '0' no split, 'nonewlines' split using punctuation only (default).",
        )
        parser.add_argument(
            "--preserve_formatting",
            action="store_true",
            default=True,
            help="Prevent automatic formatting correction (default: True).",
        )
        parser.add_argument(
            "--formality",
            choices=["less", "more"],
            default="less",
            help="Informal ('less') or formal ('more') language (default: 'less').",
        )
        parser.add_argument(
            "--context",
            type=str,
            default=None,
            help="Additional context for translations (not billed).",
        )
        parser.add_argument(
            "--model_type",
            choices=[
                "quality_optimized",
                "prefer_quality_optimized",
                "latency_optimized",
            ],
            default="prefer_quality_optimized",
            help="Translation model type (default: prefer_quality_optimized).",
        )
        parser.add_argument(
            "--translate_base_language",
            action="store_true",
            default=False,
            help=f"If specified, the base language {PO_FILE_EXTENSION} file will also be translated.",
        )
        parser.add_argument(
            "--no_backup",
            action="store_true",
            default=False,
            help=f"If specified, no backup files will be created for each {PO_FILE_EXTENSION}.",
        )
        parser.add_argument(
            "--preferred_variant",
            default=[],
            nargs="*",
            help="Preferred variant(s) for target language (e.g. en-us for US English).",
        )
        parser.add_argument(
            "--usage",
            action="store_true",
            default=False,
            help="Display remaining DeepL API usage.",
        )
        parser.add_argument(
            "--interactive",
            action="store_true",
            default=False,
            help="Enable interactive mode with confirmation before each translation.",
        )
        parser.add_argument(
            "--delay",
            type=float,
            default=0.2,
            help="Delay in seconds between each translation request (default: 0.2).",
        )
        parser.add_argument(
            "--retry-delay",
            type=float,
            default=5.0,
            help="Delay in seconds before retrying after an error (default: 5.0).",
        )
        parser.add_argument(
            "--retry-attempts",
            type=int,
            default=1,
            help="Max retry attempts per translation on error (default: 1, no retry).",
        )
        parser.add_argument(
            "--debug",
            action="store_true",
            default=False,
            help="Print failing text to stderr when translation fails.",
        )

    def initialize(self):
        self.stdout.write(
            self.style.WARNING(
                "Initializing translation client and preparing necessary data. Please wait..."
            )
        )
        self.apps_name = get_apps_name()
        self.deepl_client = self.get_deepl_client()
        self.available_source_languages = self.get_available_source_languages()
        self.available_target_languages = self.get_available_target_languages()
        self.languages_with_formality_supported = (
            self.get_languages_with_formality_supported()
        )
        self.usage = self.check_usage()
        self.available_variant = [
            lang.lower() for lang in self.available_target_languages if "-" in lang
        ]

    def handle(self, *args, **kwargs):
        self.initialize()

        if kwargs.get("usage"):
            remaining_usage = self.usage
            self.stdout.write(
                self.style.SUCCESS(
                    f"Remaining usage: {remaining_usage[0]} characters out of {remaining_usage[1]} characters."
                )
            )
            exit(0)

        formality = kwargs.get("formality")
        translate_base_language = kwargs.get("translate_base_language")
        interactive = kwargs.get("interactive")

        base_language = settings.LANGUAGE_CODE

        self.stdout.write(
            self.style.WARNING(
                f"\nDetected base translation language: {base_language}.\n"
                f"{'Including' if translate_base_language else 'Ignoring'} base language translation.\n"
            )
        )

        files_to_translate = check_translation_status(self, *args, **kwargs)

        if len(files_to_translate) <= 0:
            self.stdout.write(self.style.ERROR("\nNo files to translate."))
            exit(0)

        filtered_files_to_translate = [
            f
            for f in files_to_translate
            if translate_base_language or f["translate_to"] != base_language
        ]

        available_source_languages_set = set(self.available_source_languages)
        available_target_languages_set = set(self.available_target_languages)

        for file_to_translate in filtered_files_to_translate:
            translate_to = file_to_translate["translate_to"]
            translate_from = file_to_translate["translate_from"]

            source_lang = translate_from.split("-")[0].upper()
            if source_lang not in available_source_languages_set:
                self.stdout.write(
                    self.style.ERROR(
                        f"({translate_from}) is not an available source language."
                    )
                )
                possible_match = next(
                    (
                        lang
                        for lang in available_source_languages_set
                        if lang.startswith(source_lang)
                    ),
                    None,
                )
                if possible_match:
                    source_lang = possible_match
                    file_to_translate["translate_from"] = source_lang.lower()
                    self.stdout.write(
                        self.style.WARNING(f"Using closest match: {source_lang}")
                    )
                else:
                    self.stdout.write(
                        self.style.ERROR(
                            "No suitable match found for the source language."
                        )
                    )
                    exit(0)

            # DeepL uses hyphen (PT-PT), Django folders use underscore (pt_PT)
            target_lang = translate_to.replace("_", "-").upper()
            skip_this_file = False
            if target_lang not in available_target_languages_set:
                self.stdout.write(
                    self.style.ERROR(
                        f"({translate_to}) is not an available target language."
                    )
                )
                if interactive:
                    try:
                        while True:
                            langs = ", ".join(self.available_target_languages)
                            self.stdout.write(
                                self.style.WARNING(f"Please enter one of: {langs}")
                            )
                            self.stdout.write(
                                self.style.WARNING(
                                    "s: skip, q: deactivate interactive\n"
                                )
                            )
                            confirm = input(":").strip().lower()
                            if confirm in self.available_target_languages:
                                target_lang = confirm.upper()
                                file_to_translate["translate_to"] = confirm
                                self.stdout.write(
                                    self.style.SUCCESS("\nTranslation accepted.\n")
                                )
                                break
                            elif confirm == "s":
                                skip_this_file = True
                                self.stdout.write(self.style.WARNING("\nSkipping...\n"))
                                break
                            elif confirm == "q":
                                self.stdout.write(
                                    self.style.WARNING("\nInteractive mode disabled.\n")
                                )
                                interactive = False
                            else:
                                self.stdout.write(
                                    self.style.ERROR(
                                        f"\nInvalid choice. Enter 'q', "
                                        f"'s' or one of: {langs}\n"
                                    )
                                )
                    except KeyboardInterrupt:
                        print("\n")
                        exit(0)
                else:
                    self.stdout.write(self.style.ERROR("Skipping..."))
                    continue

                if skip_this_file:
                    continue

                possible_match = next(
                    (
                        lang
                        for lang in available_target_languages_set
                        if lang.startswith(target_lang.split("-")[0])
                    ),
                    None,
                )
                if possible_match:
                    target_lang = possible_match
                    # Keep Django folder format (underscore) for file paths
                    file_to_translate["translate_to"] = target_lang.lower().replace(
                        "-", "_"
                    )
                    self.stdout.write(
                        self.style.WARNING(f"Using closest match: {target_lang}")
                    )
                else:
                    self.stdout.write(
                        self.style.ERROR(
                            "No suitable match found for the target language."
                        )
                    )
                    exit(0)

            if (
                formality is not None
                and target_lang not in self.languages_with_formality_supported
            ):
                self.stdout.write(
                    self.style.WARNING(
                        f"({translate_to}) does not support formality. It will be ignored.\n"
                    )
                )
                kwargs["formality"] = None

            try:
                self.translate_po_file(file_to_translate, *args, **kwargs)
            except Exception as error:
                self.stdout.write(
                    self.style.ERROR(
                        f"\nError translating {file_to_translate.get('file_path', '?')}: {error}\n"
                    )
                )
                self.stdout.write(self.style.WARNING("Continuing with next file...\n"))

        self.stdout.write(self.style.SUCCESS("\nTranslation completed!"))

    def translate_po_file(self, file_to_translate, *args, **kwargs):
        overwrite = kwargs.get("overwrite")
        split_sentences = kwargs.get("split_sentences")
        preserve_formatting = kwargs.get("preserve_formatting")
        formality = kwargs.get("formality")
        context = kwargs.get("context")
        model_type = kwargs.get("model_type")
        no_backup = kwargs.get("no_backup")
        interactive = kwargs.get("interactive")
        delay = kwargs.get("delay", 0.2)
        retry_delay = kwargs.get("retry_delay", 5.0)
        retry_attempts = kwargs.get("retry_attempts", 1)
        debug = kwargs.get("debug", False)

        app = file_to_translate["app"]
        translate_from = file_to_translate["translate_from"]
        translate_to = file_to_translate["translate_to"]
        file_path = file_to_translate["file_path"]
        # DeepL API expects hyphen (pt-PT), Django folders use underscore (pt_PT)
        translate_from_deepl = translate_from.replace("_", "-").upper()
        translate_to_deepl = translate_to.replace("_", "-").upper()
        deepl_usage = self.check_usage()

        if not no_backup:
            po_file_name = f"{PO_FILE_NAME}{PO_FILE_EXTENSION}"
            backup_file_name = generate_po_backup_filename()
            backup_file_path = file_path.replace(po_file_name, backup_file_name)
            shutil.copy(file_path, backup_file_path)
        else:
            backup_file_path = "Not created"

        self.stdout.write(
            self.style.SUCCESS(
                "\n--------------------------------------\n"
                "Translation started...\n"
                f"Backup file: {backup_file_path}\n"
                f"App: {app}\n"
                f"From language: {translate_from}\n"
                f"To language: {translate_to}\n"
                f"File path: {file_path}\n"
                f"Split sentences: {split_sentences}\n"
                f"Preserve formatting: {preserve_formatting}\n"
                f"Formality: {formality}\n"
                f"Overwrite: {overwrite}\n"
                f"Context: {context}\n"
                f"Model type: {model_type}\n"
                f"Deepl usage: {deepl_usage[0]} characters used of {deepl_usage[1]} total.\n"
                "--------------------------------------"
            )
        )

        characters_consumed, characters_limit = map(int, deepl_usage)
        po = polib.pofile(file_path)
        total_entry = len(po)
        translated_count = 0

        def save_progress():
            now = datetime.now(UTC).strftime("%Y-%m-%d %H:%M+0000")
            po.metadata["PO-Revision-Date"] = now
            po.metadata["Last-Translator"] = "django-deepl-po <no-reply@kapt.mobi>"
            po.metadata["X-Translated-Using"] = "django-deepl-po"
            po.save(file_path)

        try:
            for entry in po:
                remove_fuzzy = False
                remove_previous_msgid = False
                if entry.fuzzy:
                    self.stdout.write(
                        self.style.WARNING("\nThis text is marked as fuzzy.")
                    )
                    remove_fuzzy = True
                    if entry.previous_msgid:
                        self.stdout.write(
                            self.style.WARNING("The new text will be used.")
                        )
                        remove_previous_msgid = True

                msgid = entry.msgid
                msgstr = entry.msgstr
                msgid_plural = getattr(entry, "msgid_plural", None)
                msgstr_plural = getattr(entry, "msgstr_plural", None)
                is_plural = bool(msgid_plural)

                if not msgid:
                    continue
                if is_plural:
                    if not overwrite and msgstr_plural and all(msgstr_plural.values()):
                        continue
                else:
                    if msgid and msgstr and not overwrite:
                        continue

                # Protect placeholders in HTML attributes (avoids invalid XML)
                text_protected, attr_restore_map = _protect_placeholders_in_attributes(
                    msgid
                )
                text_protected = _normalize_for_xml(text_protected)
                text_protected = _html_entities_to_numeric(text_protected)
                text_to_translate = self.add_ignore_tags(text_protected)
                estimated_billed_characters = len(text_to_translate)
                if is_plural and msgid_plural:
                    plural_protected, _ = _protect_placeholders_in_attributes(
                        msgid_plural
                    )
                    plural_protected = _normalize_for_xml(plural_protected)
                    plural_protected = _html_entities_to_numeric(plural_protected)
                    estimated_billed_characters += len(
                        self.add_ignore_tags(plural_protected)
                    )

                if (
                    estimated_billed_characters + characters_consumed
                    >= characters_limit
                ):
                    self.stdout.write(
                        self.style.ERROR(
                            "\nTranslation aborted: would exceed remaining quota.\n"
                        )
                    )
                    save_progress()
                    return

                def do_translate(text):
                    """Translate text and return result or None."""
                    text_prot, attr_map = _protect_placeholders_in_attributes(text)
                    text_prot = _normalize_for_xml(text_prot)
                    text_prot = _html_entities_to_numeric(text_prot)
                    to_send = self.add_ignore_tags(text_prot)
                    res = None
                    for attempt in range(1, retry_attempts + 1):
                        try:
                            time.sleep(delay)
                            res = self.deepl_client.translate_text(
                                text=to_send,
                                source_lang=translate_from_deepl,
                                target_lang=translate_to_deepl,
                                context=context,
                                model_type=model_type,
                                split_sentences=split_sentences,
                                preserve_formatting=preserve_formatting,
                                formality=formality,
                                tag_handling=TAG_HANDLING,
                                ignore_tags=IGNORE_TAGS_TEXT,
                            )
                            break
                        except KeyboardInterrupt:
                            raise
                        except Exception as error:
                            self.stdout.write(
                                self.style.WARNING(
                                    f"\nAttempt {attempt}/{retry_attempts}: {error}\n"
                                )
                            )
                            if debug:
                                # Standard Library
                                import sys

                                print(
                                    f"[DEBUG] Text sent to DeepL:\n{to_send!r}",
                                    file=sys.stderr,
                                )
                                print(
                                    f"[DEBUG] Length: {len(to_send)}", file=sys.stderr
                                )
                                if len(to_send) >= 197:
                                    print(
                                        f"[DEBUG] Around column 197: {to_send[185:210]!r}",
                                        file=sys.stderr,
                                    )
                            if attempt < retry_attempts:
                                time.sleep(retry_delay)
                            else:
                                return None, 0
                    if res is None:
                        return None, 0
                    out = self.remove_ignore_tags(res.text)
                    out = _restore_placeholders_in_attributes(out, attr_map)
                    return out, int(res.billed_characters)

                if is_plural:
                    # Translate singular and plural forms
                    trans_singular, chars1 = do_translate(msgid)
                    trans_plural, chars2 = do_translate(msgid_plural)
                    characters_consumed += chars1 + chars2
                    if trans_singular is None or trans_plural is None:
                        continue
                    trans_singular = _fix_placeholder_possessive(trans_singular, msgid)
                    trans_plural = _fix_placeholder_possessive(
                        trans_plural, msgid_plural
                    )
                    # Build msgstr_plural dict; use plural for any extra forms
                    if msgstr_plural:
                        n_forms = len(msgstr_plural)
                    else:
                        plural_forms = po.metadata.get("Plural-Forms", "nplurals=2")
                        match = re.search(r"nplurals=(\d+)", plural_forms)
                        n_forms = int(match.group(1)) if match else 2
                    new_msgstr_plural = {}
                    for i in range(n_forms):
                        new_msgstr_plural[i] = (
                            trans_singular if i == 0 else trans_plural
                        )
                    translated_text = new_msgstr_plural
                else:
                    result = None
                    for attempt in range(1, retry_attempts + 1):
                        try:
                            time.sleep(delay)
                            result = self.deepl_client.translate_text(
                                text=text_to_translate,
                                source_lang=translate_from_deepl,
                                target_lang=translate_to_deepl,
                                context=context,
                                model_type=model_type,
                                split_sentences=split_sentences,
                                preserve_formatting=preserve_formatting,
                                formality=formality,
                                tag_handling=TAG_HANDLING,
                                ignore_tags=IGNORE_TAGS_TEXT,
                            )
                            break
                        except KeyboardInterrupt:
                            raise
                        except Exception as error:
                            self.stdout.write(
                                self.style.WARNING(
                                    f"\nAttempt {attempt}/{retry_attempts}: {error}\n"
                                )
                            )
                            if debug:
                                # Standard Library
                                import sys

                                print(
                                    f"[DEBUG] Text sent to DeepL:\n{text_to_translate!r}",
                                    file=sys.stderr,
                                )
                                print(
                                    f"[DEBUG] Length: {len(text_to_translate)}",
                                    file=sys.stderr,
                                )
                                if len(text_to_translate) >= 197:
                                    print(
                                        f"[DEBUG] Around column 197: {text_to_translate[185:210]!r}",
                                        file=sys.stderr,
                                    )
                            if attempt < retry_attempts:
                                time.sleep(retry_delay)
                            else:
                                continue
                    if result is None:
                        continue
                    translated_text = self.remove_ignore_tags(result.text)
                    translated_text = _restore_placeholders_in_attributes(
                        translated_text, attr_restore_map
                    )
                    translated_text = _fix_placeholder_possessive(
                        translated_text, msgid
                    )
                    characters_consumed += int(result.billed_characters)

                if translated_text:
                    if interactive:
                        try:
                            while True:
                                self.stdout.write(
                                    self.style.WARNING(f"Original: {msgid}")
                                )
                                disp = (
                                    translated_text
                                    if isinstance(translated_text, str)
                                    else translated_text
                                )
                                self.stdout.write(
                                    self.style.WARNING(f"Translated: {disp}")
                                )
                                self.stdout.write(
                                    self.style.WARNING(
                                        "Accept? y: yes, s: skip, q: disable interactive\n"
                                    )
                                )
                                confirm = input(":").strip().lower()
                                if confirm == "y":
                                    self.stdout.write(
                                        self.style.SUCCESS("\nAccepted.\n")
                                    )
                                    break
                                elif confirm == "s":
                                    translated_text = None
                                    self.stdout.write(
                                        self.style.WARNING("\nSkipped.\n")
                                    )
                                    break
                                elif confirm == "q":
                                    self.stdout.write(
                                        self.style.WARNING("\nInteractive disabled.\n")
                                    )
                                    interactive = False
                                else:
                                    self.stdout.write(
                                        self.style.ERROR(
                                            "\nInvalid. Enter 'y', 's' or 'q'.\n"
                                        )
                                    )
                        except KeyboardInterrupt:
                            print("\n")
                            exit(0)

                    if translated_text:
                        if is_plural:
                            entry.msgstr_plural = translated_text
                        else:
                            entry.msgstr = translated_text
                        if remove_fuzzy:
                            entry.fuzzy = False
                        if remove_previous_msgid:
                            entry.previous_msgid = None
                        translated_count += 1
                        progress = (
                            int((translated_count / total_entry) * 100)
                            if overwrite
                            else po.percent_translated()
                        )
                        self.stdout.write(
                            self.style.WARNING(
                                f"Translation progress: {progress}% complete."
                            )
                        )

            save_progress()
        except KeyboardInterrupt:
            self.stdout.write(self.style.WARNING("\nInterrupted. Saving progress...\n"))
            save_progress()
            self.stdout.write(
                self.style.SUCCESS(f"Saved {translated_count} translations.\n")
            )
            raise

    def add_ignore_tags(self, text):
        for pattern in TRANSLATION_IGNORE_PATTERNS:
            text = re.sub(
                pattern,
                lambda m: f"{IGNORE_TAGS[0]}{m.group(0)}{IGNORE_TAGS[1]}",
                text,
            )
        return text

    def remove_ignore_tags(self, text):
        pattern = rf"{IGNORE_TAGS[0]}(.*?){IGNORE_TAGS[1]}"
        return re.sub(pattern, r"\1", text)

    def get_deepl_client(self):
        deepl_api_key = DEEPL_API_KEY
        if deepl_api_key is None or not deepl_api_key:
            self.stdout.write(
                self.style.ERROR("DEEPL_API_KEY is not defined in the settings.")
            )
            exit(0)
        try:
            return deepl.DeepLClient(deepl_api_key, send_platform_info=False)
        except Exception as error:
            self.stdout.write(self.style.ERROR(f"{error}"))
            exit(0)

    def check_usage(self):
        try:
            usage = self.deepl_client.get_usage()
            if usage.character.valid:
                return (usage.character.count, usage.character.limit)
            if usage.any_limit_reached:
                self.stdout.write(self.style.ERROR("Translation limit reached."))
                exit(0)
        except Exception as error:
            self.stdout.write(self.style.ERROR(f"{error}"))
            exit(0)

    def get_available_source_languages(self):
        try:
            return [lang.code for lang in self.deepl_client.get_source_languages()]
        except Exception as error:
            self.stdout.write(self.style.ERROR(f"{error}"))
            exit(0)

    def get_available_target_languages(self):
        try:
            return [lang.code for lang in self.deepl_client.get_target_languages()]
        except Exception as error:
            self.stdout.write(self.style.ERROR(f"{error}"))
            exit(0)

    def get_languages_with_formality_supported(self):
        try:
            return [
                lang.code
                for lang in self.deepl_client.get_target_languages()
                if lang.supports_formality
            ]
        except Exception as error:
            self.stdout.write(self.style.ERROR(f"{error}"))
            exit(0)
