"""
SecureDrop client - an easy to use interface for SecureDrop in Qubes.

Copyright (C) 2018  The Freedom of the Press Foundation.

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
"""

import gettext
import locale
import logging
import os
import platform
import signal
import socket
import sys
from argparse import ArgumentParser
from contextlib import contextmanager
from gettext import gettext as _
from logging.handlers import SysLogHandler, TimedRotatingFileHandler
from pathlib import Path
from typing import Any, NewType, NoReturn

from PyQt5.QtCore import Qt, QThread, QTimer
from PyQt5.QtWidgets import QApplication, QMessageBox

from securedrop_client import __version__, state
from securedrop_client.database import Database
from securedrop_client.db import make_session_maker
from securedrop_client.gui.main import Window
from securedrop_client.logic import Controller
from securedrop_client.utils import safe_mkdir

LanguageCode = NewType("LanguageCode", str)

DEFAULT_LANGUAGE = LanguageCode("en")
DEFAULT_SDC_HOME = "~/.securedrop_client"
DESKTOP_FILE_NAME = "press.freedom.SecureDropClient.desktop"
ENCODING = "utf-8"
GETTEXT_DOMAIN = "messages"
LOGLEVEL = os.environ.get("LOGLEVEL", "info").upper()
SDC_NAME = "SecureDrop Client"


def init(sdc_home: Path) -> None:
    safe_mkdir(sdc_home)
    safe_mkdir(sdc_home, "data")


def excepthook(*exc_args):  # type: ignore[no-untyped-def]
    """
    This function is called in the event of a catastrophic failure.
    Log exception and exit cleanly.
    """
    logging.error("Unrecoverable error", exc_info=(exc_args))
    sys.__excepthook__(*exc_args)
    print("")  # force terminal prompt on to a new line
    sys.exit(1)


def configure_locale_and_language() -> LanguageCode:
    """Configure locale, language and define location of translation assets."""
    localedir = os.path.abspath(os.path.join(os.path.dirname(__file__), "locale"))
    try:
        # Use the operating system's locale.
        current_locale, encoding = locale.getdefaultlocale()
        # Get the language code.
        if current_locale is None:
            code = DEFAULT_LANGUAGE
        else:
            code = LanguageCode(current_locale[:2])
    except ValueError:  # pragma: no cover
        code = DEFAULT_LANGUAGE  # pragma: no cover
    gettext.bindtextdomain(GETTEXT_DOMAIN, localedir=localedir)
    gettext.textdomain(GETTEXT_DOMAIN)
    return code


def configure_logging(sdc_home: Path) -> None:
    """
    Set up all logging.
    """
    safe_mkdir(sdc_home, "logs")
    log_file = os.path.join(sdc_home, "logs", "client.log")

    # set logging format
    log_fmt = "%(asctime)s - %(name)s:%(lineno)d(%(funcName)s) " "%(levelname)s: %(message)s"
    formatter = logging.Formatter(log_fmt)

    # define log handlers such as for rotating log files
    handler = TimedRotatingFileHandler(
        log_file, when="midnight", backupCount=5, delay=False, encoding=ENCODING
    )
    handler.setFormatter(formatter)

    # For syslog handler
    if platform.system() != "Linux":  # pragma: no cover
        syslog_file = "/var/run/syslog"
    else:
        syslog_file = "/dev/log"

    sysloghandler = SysLogHandler(address=syslog_file)
    sysloghandler.setFormatter(formatter)

    # set up primary log
    log = logging.getLogger()
    log.setLevel(LOGLEVEL)
    log.addHandler(handler)

    # add the secondary logger
    log.addHandler(sysloghandler)

    # override excepthook to capture a log of catastrophic failures.
    sys.excepthook = excepthook


def configure_signal_handlers(app: QApplication) -> None:
    def signal_handler(*nargs) -> None:  # type: ignore[no-untyped-def]
        app.quit()

    for sig in [signal.SIGINT, signal.SIGTERM]:
        signal.signal(sig, signal_handler)


def expand_to_absolute(value: str) -> str:
    """
    Expands a path to the absolute path so users can provide
    arguments in the form ``~/my/dir/``.
    """
    return os.path.abspath(os.path.expanduser(value))


def arg_parser() -> ArgumentParser:
    parser = ArgumentParser("securedrop-client", description="SecureDrop Journalist GUI")
    parser.add_argument(
        "-H",
        "--sdc-home",
        default=DEFAULT_SDC_HOME,
        type=expand_to_absolute,
        help=(
            f"{SDC_NAME} home directory for storing files and state. "
            f"(Default {DEFAULT_SDC_HOME})"
        ),
    )
    parser.add_argument(
        "--no-proxy", action="store_true", help="Use proxy AppVM name to connect to server."
    )
    parser.add_argument(
        "--no-qubes", action="store_true", help="Disable opening submissions in DispVMs"
    )
    return parser


def prevent_second_instance(app: QApplication, unique_name: str) -> None:
    # This function is only necessary on Qubes, so we can skip it on other platforms to help devs
    if platform.system() != "Linux":  # pragma: no cover
        return

    # Null byte triggers abstract namespace
    IDENTIFIER = "\0" + app.applicationName() + unique_name
    ALREADY_BOUND_ERRNO = 98

    app.instance_binding = socket.socket(  # type: ignore[attr-defined]
        socket.AF_UNIX, socket.SOCK_DGRAM
    )
    try:
        app.instance_binding.bind(IDENTIFIER)  # type: ignore[attr-defined]
    except OSError as e:
        if e.errno == ALREADY_BOUND_ERRNO:
            err_dialog = QMessageBox()
            err_dialog.setText(
                _("{application_name} is already running").format(
                    application_name=app.applicationName()
                )
            )
            err_dialog.exec()
            sys.exit()
        else:
            raise


@contextmanager
def threads(count: int) -> Any:
    """Ensures that the thread is properly closed before its reference is dropped."""
    threads = []
    for i in range(count):
        threads.append(QThread())

    yield threads

    for thread in threads:
        thread.exit()

        # Wait until the thread has finished, or the deadline expires.
        TWO_SECONDS_IN_MILLISECONDS = 2000
        thread.wait(TWO_SECONDS_IN_MILLISECONDS)


def start_app(args, qt_args) -> NoReturn:  # type: ignore[no-untyped-def]
    """
    Create all the top-level assets for the application, set things up and
    run the application. Specific tasks include:

    - set up locale and language.
    - set up logging.
    - create an application object.
    - create a window for the app.
    - create an API connection to the SecureDrop proxy.
    - create a SqlAlchemy session to local storage.
    - configure the client (logic) object.
    - ensure the application is setup in the default safe starting state.
    """
    os.umask(0o077)
    configure_locale_and_language()
    init(args.sdc_home)
    configure_logging(args.sdc_home)
    logging.info(f"Starting {SDC_NAME} {__version__}")

    app = QApplication(qt_args)
    app.setApplicationName(SDC_NAME)
    app.setDesktopFileName(DESKTOP_FILE_NAME)
    app.setApplicationVersion(__version__)
    app.setAttribute(Qt.AA_UseHighDpiPixmaps)

    prevent_second_instance(app, args.sdc_home)

    session_maker = make_session_maker(args.sdc_home)

    session = session_maker()
    database = Database(session)
    app_state = state.State(database)

    with threads(3) as [
        sync_thread,
        main_queue_thread,
        file_download_queue_thread,
    ]:
        gui = Window(app_state)

        controller = Controller(
            "http://localhost:8081/",
            gui,
            session_maker,
            args.sdc_home,
            app_state,
            not args.no_proxy,
            not args.no_qubes,
            sync_thread,
            main_queue_thread,
            file_download_queue_thread,
        )
        controller.setup()

        configure_signal_handlers(app)
        timer = QTimer()
        timer.start(500)
        timer.timeout.connect(lambda: None)

        sys.exit(app.exec_())


def run() -> NoReturn:
    args, qt_args = arg_parser().parse_known_args()
    # reinsert the program's name
    qt_args.insert(0, "securedrop-client")
    start_app(args, qt_args)
