# Copyright 2022 DeepL SE (https://www.deepl.com)
# Use of this source code is governed by an MIT
# license that can be found in the LICENSE file.

import argparse
import deepl
import logging
import os
import pathlib
import sys
from typing import List, Optional
from deepl.util import _optional_import

# Program name for integration with click.testing
name = "python -m deepl"

env_auth_key = "DEEPL_AUTH_KEY"
env_server_url = "DEEPL_SERVER_URL"
env_proxy_url = "DEEPL_PROXY_URL"

keyring_key_folder = "deepl"
keyring_key_name = env_auth_key


def action_usage(translator: deepl.Translator):
    """Action function for the usage command."""
    usage_result = translator.get_usage()
    print(usage_result)


def action_languages(translator: deepl.Translator, glossary: bool):
    """Action function for the languages command."""
    if glossary:
        glossary_languages = translator.get_glossary_languages()
        print("Language pairs supported for glossaries: (source, target)")
        for language_pair in glossary_languages:
            print(f"{language_pair.source_lang}, {language_pair.target_lang}")
    else:
        source_languages = translator.get_source_languages()
        target_languages = translator.get_target_languages()

        print("Source languages available:")
        for language in source_languages:
            print(f"{language.code}: {language.name}")
        print("Target languages available:")
        for language in target_languages:
            if language.supports_formality:
                print(f"{language.code}: {language.name} (supports formality)")
            else:
                print(f"{language.code}: {language.name}")


def action_document(
    translator: deepl.Translator,
    file: List[str],
    dest: str,
    output_format: Optional[str],
    **kwargs,
):
    """Action function for the document command."""
    if not os.path.exists(dest):
        os.makedirs(dest, exist_ok=True)
    elif not os.path.isdir(dest):
        raise Exception("Destination already exists, and is not a directory")

    for this_file in file:
        outfile_name = (
            this_file
            if not output_format
            else (os.path.splitext(this_file)[0] + "." + output_format)
        )
        output_path = os.path.join(dest, os.path.basename(outfile_name))
        translator.translate_document_from_filepath(
            this_file, output_path, **kwargs
        )


def action_text(
    translator: deepl.Translator,
    show_detected_source: bool = False,
    **kwargs,
):
    """Action function for the text command."""
    translation = translator.translate_text(**kwargs)
    output_list = (
        translation if isinstance(translation, List) else [translation]
    )
    for output in output_list:
        if show_detected_source:
            print(f"Detected source language: {output.detected_source_lang}")
        print(output.text)


def action_glossary(
    translator: deepl.Translator,
    subcommand: str,
    **kwargs,
):
    # Call action function corresponding to command with remaining args
    globals()[f"action_glossary_{subcommand}"](translator, **kwargs)
    pass


def action_glossary_create(
    translator: deepl.Translator, entry_list, file, csv, **kwargs
):
    term_separator = None
    if file:
        if entry_list:
            raise deepl.DeepLException(
                "The --file argument cannot be used together with "
                "command-line entries"
            )
        content = pathlib.Path(file).read_text("UTF-8")
    elif entry_list and entry_list[0] == "-":
        content = sys.stdin.read()
    else:
        content = "\n".join(entry_list)
        term_separator = "="
        if csv:
            raise Exception(
                "csv option is not compatible with command-line entries"
            )

    if csv:
        glossary = translator.create_glossary_from_csv(
            csv_data=content, **kwargs
        )
    else:
        if term_separator:
            entry_dict = deepl.convert_tsv_to_dict(content, term_separator)
        else:
            entry_dict = deepl.convert_tsv_to_dict(content)
        glossary = translator.create_glossary(entries=entry_dict, **kwargs)

    print(f"Created {glossary}")
    print_glossaries([glossary])


def print_glossaries(glossaries):
    headers = [
        "Glossary ID",
        "Name",
        "Ready",
        "Source",
        "Target",
        "Count",
        "Created",
    ]
    data = [
        [
            glossary.glossary_id,
            glossary.name,
            str(glossary.ready),
            glossary.source_lang,
            glossary.target_lang,
            str(glossary.entry_count),
            str(glossary.creation_time),
        ]
        for glossary in glossaries
    ]
    data.insert(0, headers)

    col_max_widths = [
        max(len(row[col_num]) for row in data)
        for col_num in range(len(headers))
    ]
    for row in data:
        print(
            "\t".join(
                [col.ljust(width) for col, width in zip(row, col_max_widths)]
            )
        )


def action_glossary_list(translator: deepl.Translator):
    glossaries = translator.list_glossaries()
    print_glossaries(glossaries)


def action_glossary_get(translator: deepl.Translator, **kwargs):
    glossary = translator.get_glossary(**kwargs)
    print_glossaries([glossary])


def action_glossary_entries(translator: deepl.Translator, glossary_id):
    glossary_entries = translator.get_glossary_entries(glossary=glossary_id)
    print(deepl.convert_dict_to_tsv(glossary_entries))


def action_glossary_delete(
    translator: deepl.Translator, glossary_id_list: str
):
    for glossary_id in glossary_id_list:
        translator.delete_glossary(glossary_id)
        print(f"Glossary with ID {glossary_id} successfully deleted.")


def get_parser(prog_name):
    """Constructs and returns the argument parser for all commands."""
    parser = argparse.ArgumentParser(
        prog=prog_name,
        description="Translate text using the DeepL API "
        "(https://www.deepl.com/docs-api).",
        epilog="If you encounter issues while using this program, please "
        "report them at https://github.com/DeepLcom/deepl-python/issues",
    )
    parser.add_argument(
        "--version",
        action="version",
        version=f"deepl-python v{deepl.__version__}",
    )
    parser.add_argument(
        "--verbose",
        "-v",
        action="count",
        dest="verbose",
        default=0,
        help="print additional information, can be supplied multiple times "
        "for more verbose output",
    )
    parser.add_argument(
        "--no-platform-info",
        default=False,
        action="store_true",
        dest="noplatforminfo",
        help="if this flag is enabled, do not send additional information "
        "about the platform with API requests.",
    )

    parser.add_argument(
        "--auth-key",
        default=None,
        help="authentication key as given in your DeepL account; the "
        f"{env_auth_key} environment variable is used as secondary fallback; "
        f"the key {keyring_key_name} in {keyring_key_folder} is used "
        "as tertiary fallback",
    )
    parser.add_argument(
        "--server-url",
        default=None,
        metavar="URL",
        help=f"alternative server URL for testing; the {env_server_url} "
        f"environment variable may be used as secondary fallback",
    )
    parser.add_argument(
        "--proxy-url",
        default=None,
        metavar="URL",
        help="proxy server URL to use for all connections; the "
        f"{env_proxy_url} environment variable may be used as secondary "
        "fallback",
    )

    # Note: add_subparsers param 'required' is not available in py36
    subparsers = parser.add_subparsers(metavar="command", dest="command")

    def add_common_arguments(subparser: argparse.ArgumentParser):
        """Adds arguments shared between text and document commands to the
        subparser."""
        subparser.add_argument(
            "--to",
            "--target-lang",
            dest="target_lang",
            required=True,
            help="language into which the text should be translated",
        )
        subparser.add_argument(
            "--from",
            "--source-lang",
            dest="source_lang",
            help="language of the text to be translated; unless using a "
            "glossary, this argument is optional and if it is omitted DeepL "
            "will auto-detect the source language.",
        )
        subparser.add_argument(
            "--formality",
            type=str,
            choices=[enum.value for enum in deepl.Formality],
            default=deepl.Formality.DEFAULT.value,
            help="desired formality for translation",
        )
        subparser.add_argument(
            "--glossary-id",
            dest="glossary",
            type=str,
            help="ID of glossary to use for translation",
        )

    # create the parser for the "text" command
    parser_text = subparsers.add_parser(
        "text", help="translate text(s)", description="translate text(s)"
    )
    add_common_arguments(parser_text)
    parser_text.add_argument(
        "--context",
        type=str,
        help="additional contextual text to improve translations, see API docs"
        " for information",
    )
    parser_text.add_argument(
        "--split-sentences",
        type=str,
        choices=[enum.value for enum in deepl.SplitSentences],
        default=deepl.SplitSentences.DEFAULT.value,
        help="control sentence splitting before translation, see API for "
        "information",
    )
    parser_text.add_argument(
        "--preserve-formatting",
        action="store_true",
        help="leave original formatting unchanged during translation",
    )
    parser_text.add_argument(
        "text",
        nargs="+",
        type=str,
        help="text to be translated. Wrap text in quotes to prevent the shell "
        'from splitting sentences into words. Alternatively, use "-" to read '
        "from standard-input.",
    )
    parser_text.add_argument(
        "--show-detected-source",
        action="store_true",
        help="print detected source language for each text",
    )

    tag_handling_group = parser_text.add_argument_group(
        "tag-handling",
        description="Arguments controlling tag handling, for example XML. "
        "The -tags arguments accept multiple arguments, as comma-"
        "separated lists and as repeated arguments. For example, these are "
        'equivalent: "--ignore-tags a --ignore-tags b,c" and "--ignore-tags '
        'a,b,c".',
    )
    tag_handling_group.add_argument(
        "--tag-handling",
        type=str,
        choices=["xml", "html"],
        default=None,
        help="activate processing of formatting tags, for example 'xml'",
    )
    tag_handling_group.add_argument(
        "--outline-detection-off",
        dest="outline_detection",
        default=True,
        action="store_false",
        help="disable automatic tag selection",
    )
    tag_handling_group.add_argument(
        "--non-splitting-tags",
        type=str,
        action="append",
        metavar="tag",
        help="specify tags that may occur within sentences",
    )
    tag_handling_group.add_argument(
        "--splitting-tags",
        type=str,
        action="append",
        metavar="tag",
        help="specify tags that separate text into sentences",
    )
    tag_handling_group.add_argument(
        "--ignore-tags",
        type=str,
        action="append",
        metavar="tag",
        help="specify tags containing text that should not be translated",
    )

    # create the parser for the "document" command
    parser_document = subparsers.add_parser(
        "document",
        help="translate document(s)",
        description="translate document(s)",
    )
    add_common_arguments(parser_document)
    parser_document.add_argument(
        "file", nargs="+", help="file(s) to be translated."
    )
    parser_document.add_argument(
        "--output-format", type=str, default=None, help="output file extension"
    )
    parser_document.add_argument(
        "dest", help="destination directory to store translated files."
    )

    # create the parser for the "usage" command
    usage_help_str = "print usage information for the current billing period"
    subparsers.add_parser(
        "usage", help=usage_help_str, description=usage_help_str
    )

    # create the parser for the "languages" command
    languages_help_str = "print available languages"
    parser_languages = subparsers.add_parser(
        "languages", help=languages_help_str, description=languages_help_str
    )
    parser_languages.add_argument(
        "--glossary",
        help="list language pairs supported for glossaries.",
        action="store_true",
    )

    # create the parser for the "glossary" command
    parser_glossary = subparsers.add_parser(
        "glossary",
        help="create, list, and remove glossaries",
        description="manage glossaries using subcommands",
    )

    # Note: add_subparsers param 'required' is not available in py36
    glossary_subparsers = parser_glossary.add_subparsers(
        metavar="subcommand", dest="subcommand"
    )
    parser_glossary_create = glossary_subparsers.add_parser(
        "create",
        help="create a new glossary",
        description="create a new glossary using entries provided via command-"
        "line, standard-input, or specified in a TSV or CSV file",
    )
    parser_glossary_create.add_argument(
        "--name", required=True, help="name to be associated with glossary."
    )
    parser_glossary_create.add_argument(
        "--from",
        "--source-lang",
        dest="source_lang",
        required=True,
        help="language in which source entries of the glossary are specified.",
    )
    parser_glossary_create.add_argument(
        "--to",
        "--target-lang",
        dest="target_lang",
        required=True,
        help="language in which target entries of the glossary are specified.",
    )
    parser_glossary_create.add_argument(
        "entry_list",
        nargs="*",
        type=str,
        metavar="SOURCE=TARGET",
        help="one or more entries to add to glossary, may be repeated. "
        'Alternatively, use "-" to read entries from standard-input in TSV or '
        "CSV format (see --file argument for formatting information). These "
        "arguments cannot be used together with the --file argument.",
    )
    parser_glossary_create.add_argument(
        "--file",
        type=str,
        help="file to read glossary entries from. Unless --csv is specified, "
        "file format is expected to be tab-separated values (TSV) format: one "
        "entry-pair per line, each line contains the source entry, a tab, "
        "then the target entry. Empty lines are ignored.",
    )
    parser_glossary_create.add_argument(
        "--csv",
        action="store_true",
        help="the provided --file option or standard-input should be "
        "interpreted as a CSV file. Information about the expected CSV format "
        "can be found in the API documentation: "
        "https://www.deepl.com/docs-api/managing-glossaries/supported-glossary-formats/.",  # noqa
    )

    parser_glossary_list = glossary_subparsers.add_parser(
        "list",
        help="list available glossaries",
        description="list available glossaries",
    )
    _ = parser_glossary_list  # Suppress unused variable warning

    parser_glossary_get = glossary_subparsers.add_parser(
        "get",
        help="print details about one glossary",
        description="print details about one glossary",
    )
    parser_glossary_get.add_argument(
        "glossary_id",
        metavar="id",
        type=str,
        help="ID of glossary to retrieve",
    )

    parser_glossary_entries = glossary_subparsers.add_parser(
        "entries",
        help="get entries contained in a glossary",
        description="get entries contained in a glossary, and print them to "
        "standard-output in tab-separated values (TSV) format: one entry-pair "
        "per line, each line contains the source entry, a tab, then the "
        "target entry.",
    )
    parser_glossary_entries.add_argument(
        "glossary_id",
        metavar="id",
        type=str,
        help="ID of glossary to retrieve",
    )

    parser_glossary_delete = glossary_subparsers.add_parser(
        "delete",
        help="delete one or more glossaries",
        description="delete one or more glossaries",
    )
    parser_glossary_delete.add_argument(
        "glossary_id_list",
        metavar="id",
        nargs="+",
        type=str,
        help="ID of glossary to delete",
    )

    return parser, parser_glossary


def main(args=None, prog_name=None):
    parser, parser_glossary = get_parser(prog_name)
    args = parser.parse_args(args)

    if args.command is None:
        # Support for Python 3.6 - subcommands cannot be required
        sys.stderr.write("Error: command is required\n")
        parser.print_help(sys.stderr)
        sys.exit(1)

    logger = logging.getLogger("deepl")
    if args.verbose == 1:
        logger.setLevel(logging.INFO)
        logger.addHandler(logging.StreamHandler())
    elif args.verbose >= 2:
        logger.setLevel(logging.DEBUG)
        logger.addHandler(logging.StreamHandler())
    else:
        logger.setLevel(logging.WARNING)

    server_url = args.server_url or os.getenv(env_server_url)
    proxy_url = args.proxy_url or os.getenv(env_proxy_url)

    auth_key = args.auth_key or os.getenv(env_auth_key)
    keyring = _optional_import("keyring")
    if keyring:
        keyring_pw = None
        try:
            keyring_pw = keyring.get_password(
                keyring_key_folder, keyring_key_name
            )
        except keyring.errors.NoKeyringError:
            pass
        auth_key = auth_key or keyring_pw

    try:
        if auth_key is None:
            raise Exception(
                f"Please provide authentication key via the {env_auth_key} "
                "environment variable or --auth-key argument or via "
                f"{keyring_key_name} in {keyring_key_folder} in keyring"
            )

        # Note: the get_languages() call to verify language codes is skipped
        #       because the CLI makes one API call per execution.
        translator = deepl.Translator(
            auth_key=auth_key,
            server_url=server_url,
            proxy=proxy_url,
            skip_language_check=True,
            send_platform_info=not args.noplatforminfo,
        )

        if args.command == "text":
            if len(args.text) == 1 and args.text[0] == "-":
                args.text = [sys.stdin.read()]

        elif args.command == "glossary":
            if args.subcommand is None:
                # Support for Python 3.6 - subcommands cannot be required
                sys.stderr.write("Error: glossary subcommand is required\n")
                parser_glossary.print_help(sys.stderr)
                sys.exit(1)

        # Remove global args so they are not unrecognised in action functions
        del (
            args.verbose,
            args.server_url,
            args.auth_key,
            args.proxy_url,
            args.noplatforminfo,
        )
        args = vars(args)
        # Call action function corresponding to command with remaining args
        command = args.pop("command")
        globals()[f"action_{command}"](translator, **args)

    except Exception as exception:
        sys.stderr.write(f"Error: {exception}\n")
        sys.exit(1)


if __name__ == "__main__":
    main(prog_name="deepl")
