# -*- coding: utf-8 -*-
#
# Licensed under the terms of the BSD 3-Clause
# (see guidata/LICENSE for details)

# pylint: disable=C0103

"""
Translations utilities
----------------------

Description
^^^^^^^^^^^

This module provides utilities for managing translations in guidata.
It is based on `Babel <https://babel.pocoo.org/en/latest/>`_.

Three functions are provided:

- :func:`scan_translations`: Extracts translatable strings from Python files
  and generates translation files.

- :func:`compile_translations`: Compiles translation files into binary format.

- :func:`main`: Convenience function to call :func:`scan_translations` or
  :func:`compile_translations` with command-line arguments.

Usage
^^^^^

To extract translatable strings and generate translation files, use:

.. code-block:: console

    $ # Scan translations
    $ python -m guidata.utils.translations scan --name <name> --directory <directory>
    $ # Alternatively, you can use the command-line tool:
    $ guidata-translations scan --name <name> --directory <directory>

To compile translation files into binary format, use:

.. code-block:: console

    $ # Compile translations
    $ python -m guidata.utils.translations compile --name <name> --directory <directory>
    $ # Alternatively, you can use the command-line tool:
    $ guidata-translations compile --name <name> --directory <directory>
"""

from __future__ import annotations

import argparse
import locale
import os
import re
import subprocess
import sys
import typing

PYBABEL_ARGS = [sys.executable, "-m", "babel.messages.frontend"]

# Header lines to remove from .po files to avoid unnecessary merge conflicts
# when cherry-picking commits between branches
PO_HEADERS_TO_REMOVE = [
    "POT-Creation-Date",
    "PO-Revision-Date",
    "Last-Translator",
    "Generated-By",
    "Project-Id-Version",
    "Report-Msgid-Bugs-To",
    "Language-Team",
]


def _cleanup_po_file(po_file: str) -> None:
    """Remove volatile header lines from a .po file to avoid merge conflicts.

    Args:
        po_file: Path to the .po file to clean up.
    """
    with open(po_file, "r", encoding="utf-8") as f:
        content = f.read()

    # Build a regex pattern to match and remove the header lines
    for header in PO_HEADERS_TO_REMOVE:
        # Match the header line in the msgstr block (e.g., "POT-Creation-Date: ...\n")
        pattern = rf'"{header}:[^"]*\\n"\n'
        content = re.sub(pattern, "", content)

    with open(po_file, "w", encoding="utf-8") as f:
        f.write(content)


def get_default_language_code() -> str:
    """Get the default language code of the system.

    Returns:
        The default language code (e.g., 'en', 'fr').
    """
    lang, _ = locale.getlocale()
    return lang.split("_")[0] if lang else "en"


def scan_translations(
    name: str,
    directory: typing.Union[str, os.PathLike],
    copyright_holder: str | None = None,
    languages: typing.List[str] | None = None,
) -> None:
    """Extract and process translatable strings from Python files using Babel.

    This function performs the following steps:

    1. Check the project root directory.
    2. Check the Babel configuration file.
    3. Set up the translation directory.
    4. Extract translatable strings from the project and generate a template file.
    5. For each specified language, check that the necessary directory structure exists
       and generate or update the corresponding translation file.

    Args:
        name: The name of the project, used for directory and domain naming.
        directory: The root directory of the project.
        copyright_holder: The name of the copyright holder for the project.
        languages: A list of language codes (e.g., ['fr', 'it'])
         for which translation files should be generated or updated.

    Raises:
        FileNotFoundError: The Babel configuration file does not exist.
        RuntimeError: Extraction or translation file generation failed.
    """
    if copyright_holder is None:
        copyright_holder = ""
    if languages is None:
        # If no language codes are specified, use the system's default language code
        languages = [get_default_language_code()]

    # Check the project root directory
    if not os.path.exists(directory):
        print(f"Error: Project root directory {directory} does not exist.")
        raise FileNotFoundError(f"Project root directory {directory} does not exist.")
    print(f"Project root directory: {directory}")

    # Set the Babel configuration file path
    babel_cfg = os.path.join(directory, "babel.cfg")
    if not os.path.exists(babel_cfg):
        print(f"Error: Babel configuration file {babel_cfg} does not exist.")
        raise FileNotFoundError(f"Babel configuration file {babel_cfg} does not exist.")

    # Set the translation directory
    translation_dir = os.path.join(directory, name, "locale")
    if not os.path.exists(translation_dir):
        os.makedirs(translation_dir)
        print(f"Created translation directory: {translation_dir}")

    # Set the template file path
    catalog_template = os.path.join(translation_dir, f"{name}.pot")
    print(f"Output template file: {catalog_template}")

    # Run pybabel extract
    print("Extracting translatable strings...")
    try:
        subprocess.run(
            PYBABEL_ARGS
            + [
                "extract",
                directory,
                "--mapping-file",
                babel_cfg,
                "--output-file",
                catalog_template,
                "--no-location",
                "--no-wrap",
                "--sort-by-file",
                "--header-comment",
                """\
# Translations template for PROJECT.
# Copyright (C) YEAR ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
#""",
                "--project",
                name,
                "--copyright-holder",
                copyright_holder,
            ],
            check=True,
        )
        print(f"Extraction completed successfully. Output file: {catalog_template}")
        _cleanup_po_file(catalog_template)
    except subprocess.CalledProcessError as e:
        print("Error: Extraction failed.")
        print(e.stderr)
        raise RuntimeError(f"Extraction failed: {e.stderr}") from e

    # Initialize the folder structure if it does not exist
    for code in languages:
        locale_dir = os.path.join(translation_dir, code, "LC_MESSAGES")
        if not os.path.exists(locale_dir):
            os.makedirs(locale_dir)
            print(f"Created translation directory: {locale_dir}")

    # Generate or update the translation file
    print(f"Translation directory: {translation_dir}")
    try:
        for code in languages:
            subprocess.run(
                PYBABEL_ARGS
                + [
                    "update",
                    "--input-file",
                    catalog_template,
                    "--output-dir",
                    translation_dir,
                    "--locale",
                    code,
                    "--domain",
                    name,
                    "--ignore-obsolete",
                    "--init-missing",
                    "--no-wrap",
                    "--update-header-comment",
                    "--ignore-pot-creation-date",
                ],
                check=True,
            )
            # Clean up the .po file to remove volatile headers
            po_file = os.path.join(translation_dir, code, "LC_MESSAGES", f"{name}.po")
            _cleanup_po_file(po_file)
    except subprocess.CalledProcessError as e:
        print("Error: Translation file generation failed.")
        print(e.stderr)
        raise RuntimeError(f"Translation file generation failed: {e.stderr}") from e


def compile_translations(
    name: str,
    directory: typing.Union[str, os.PathLike],
) -> None:
    """Compile translated strings using Babel.

    This function performs the following steps:
    1. Check the project root directory.
    2. Check the Babel configuration file.
    3. Verify the presence of the translation directory.
    4. Compile the translation files.

    Args:
        name: The name of the project.
        directory: The root directory of the project.

    Raises:
        FileNotFoundError: The Babel configuration file or the translation directory
        does not exist.
        RuntimeError: The compilation failed.
    """
    # Check the project root directory
    if not os.path.exists(directory):
        print(f"Error: Project root directory {directory} does not exist.")
        raise FileNotFoundError(f"Project root directory {directory} does not exist.")
    print(f"Project root directory: {directory}")

    # Set the Babel configuration file path
    babel_cfg = os.path.join(directory, "babel.cfg")
    if not os.path.exists(babel_cfg):
        print(f"Error: Babel configuration file {babel_cfg} does not exist.")
        raise FileNotFoundError(f"Babel configuration file {babel_cfg} does not exist.")

    # Set the translation directory
    translation_dir = os.path.join(directory, name, "locale")
    if not os.path.exists(translation_dir):
        print(f"Error: Translation directory {translation_dir} does not exist.")
        raise FileNotFoundError(
            f"Translation directory {translation_dir} does not exist."
        )
    print(f"Translation directory {translation_dir}")

    # Compile the translation files
    try:
        subprocess.run(
            PYBABEL_ARGS
            + [
                "compile",
                "--directory",
                translation_dir,
                "--domain",
                name,
                "--statistics",
            ],
            check=True,
        )
        print("Compilation completed successfully.")
    except subprocess.CalledProcessError as e:
        print("Error: Compilation failed.")
        print(e.stderr)
        raise RuntimeError(f"Compilation failed: {e.stderr}") from e


def cleanup_doc_translations(
    directory: typing.Union[str, os.PathLike],
    locale_dir: str = "doc/locale",
) -> None:
    """Clean up Sphinx documentation translation files.

    This function removes volatile header lines (POT-Creation-Date, Last-Translator)
    from all .po files in the specified locale directory to avoid unnecessary merge
    conflicts when cherry-picking commits between branches.

    Args:
        directory: The root directory of the project.
        locale_dir: The relative path to the locale directory (default: "doc/locale").

    Raises:
        FileNotFoundError: The locale directory does not exist.
    """
    locale_path = os.path.join(directory, locale_dir)
    if not os.path.exists(locale_path):
        print(f"Error: Locale directory {locale_path} does not exist.")
        raise FileNotFoundError(f"Locale directory {locale_path} does not exist.")

    # Find all .po files in the locale directory
    po_files = []
    for root, _, files in os.walk(locale_path):
        for file in files:
            if file.endswith(".po"):
                po_files.append(os.path.join(root, file))

    if not po_files:
        print(f"No .po files found in {locale_path}")
        return

    print(f"Cleaning up {len(po_files)} .po files in {locale_path}...")
    for po_file in po_files:
        _cleanup_po_file(po_file)
    print("Cleanup completed successfully.")


def _get_def(option: str) -> str | None:
    """Get the default value from environment variables or return None."""
    return os.environ.get(f"I18N_{option.upper()}")


def _is_req(option: str) -> bool:
    """Check if the option is required based on environment variables."""
    return _get_def(option) is None


def main():
    """Run one of the main functions based on command-line arguments."""
    parser = argparse.ArgumentParser(
        description="Extract and process translations with Babel.",
        epilog="Use 'scan' to update .po files and 'compile' to generate .mo files.",
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
    )
    subparsers = parser.add_subparsers(dest="command", required=True)

    compile_parser = subparsers.add_parser("compile", help="Compile translations")
    compile_parser.add_argument(
        "--name",
        required=_is_req("name"),
        default=_get_def("name"),
        help="Project name",
    )
    compile_parser.add_argument(
        "--directory", required=True, help="Project root directory"
    )

    scan_parser = subparsers.add_parser("scan", help="Scan for translatable strings")
    scan_parser.add_argument("--name", required=True, help="Project name")
    scan_parser.add_argument(
        "--directory", required=True, help="Project root directory"
    )
    scan_parser.add_argument("--version", required=False, help="Project version")
    scan_parser.add_argument(
        "--copyright-holder", required=False, help="Copyright holder name"
    )
    scan_parser.add_argument(
        "--languages",
        required=False,
        default=None,
        nargs="+",
        help="Language codes to translate (space-separated, e.g., 'fr it')",
    )

    cleanup_doc_parser = subparsers.add_parser(
        "cleanup-doc", help="Clean up Sphinx documentation translation files"
    )
    cleanup_doc_parser.add_argument(
        "--directory", required=True, help="Project root directory"
    )
    cleanup_doc_parser.add_argument(
        "--locale-dir",
        required=False,
        default="doc/locale",
        help="Relative path to the locale directory",
    )

    args = parser.parse_args()

    if args.command == "compile":
        compile_translations(args.name, args.directory)
    elif args.command == "scan":
        scan_translations(
            args.name,
            args.directory,
            args.copyright_holder,
            args.languages,
        )
    elif args.command == "cleanup-doc":
        cleanup_doc_translations(args.directory, args.locale_dir)
    else:
        parser.print_help()
        raise ValueError(f"Unknown command: {args.command}. Use 'scan' or 'compile'.")


if __name__ == "__main__":
    main()
