# Part of Carburetor project
# Released under GPLv3+ License
# Danial Behzadi<dani.behzi@ubuntu.com>, 2020-2024.

"""
actions for carburetor
"""

import os
import re
import signal

from subprocess import PIPE, Popen

from gi.repository import Adw, Gio, GLib, GObject, Gtk
from tractor import actions as tactions
from tractor import bridges, checks, db, proxy

from . import config, ui


def add(name: str, callback, shortcut, app) -> None:
    """
    adds callbacks to app as actions
    """
    action = Gio.SimpleAction.new(name, None)
    action.connect("activate", callback, app)
    app.add_action(action)
    if shortcut:
        app.set_accels_for_action(f"app.{name}", shortcut)


def do_startup(app) -> None:
    """
    actions to do when starting the app up
    """
    action_list = [
        ("show", on_show, None),
        ("preferences", on_preferences, ["<primary>comma"]),
        ("about", on_about, None),
        ("quit", on_quit, ["<primary>q"]),
        ("connect", on_connect, ["F9"]),
        ("logs", on_logs, None),
        ("clear-logs", on_clear_logs, ["<primary>l"]),
        ("copy-logs", on_copy_logs, ["<primary><shift>c"]),
        ("new_id", on_new_id, ["F5"]),
        ("check-connection", on_check, None),
        ("cancel", on_cancel, None),
        ("open-add", open_add_bridge_dialog, None),
        ("open_externally", on_open_externally, None),
        ("telegram", on_telegram, None),
        ("bridgedb", on_bridgedb, None),
        ("email", on_email, None),
    ]
    for action, callback, shortcut in action_list:
        add(action, callback, shortcut, app)
    action = Gio.SimpleAction.new_stateful(
        "set-proxy", None, GLib.Variant.new_boolean(db.get_val("auto-set"))
    )
    action.connect("change-state", on_set_proxy, app)
    app.add_action(action)


def on_show(*_, **__) -> None:
    """
    Show the window to user
    """
    window = ui.get("MainWindow")
    window.present()


def on_preferences(*argv) -> None:
    """
    show the preferences dialog
    """
    app = argv[2]
    if not app.prefs:
        prefs_dialog = ui.get("PreferencesDialog")
        app.prefs = prefs_dialog
    app.prefs.present(app.window)


def on_about(*argv) -> None:
    """
    show the about window
    """
    app = argv[2]
    if not app.about:
        prefix = "/io/frama/tractor/carburetor"
        about_dialog = Adw.AboutDialog.new_from_appdata(
            f"{prefix}/io.frama.tractor.carburetor.metainfo.xml",
            app.version,
        )
        about_dialog.set_developers(["Danial Behzadi <dani.behzi@ubuntu.com>"])
        about_dialog.set_translator_credits(_("translator-credits"))
        app.about = about_dialog
    app.about.present(app.window)


def on_quit(*argv) -> None:
    """
    exit the app
    """
    tactions.kill_tor()
    app = argv[2]
    app.quit()


def on_connect(*argv) -> None:
    """
    clicking on connect button
    """
    if stop_on_no_executable() or stop_on_no_bridges():
        return None
    app = argv[2]
    button = ui.get("ConnectButton")
    button.set_label(_("_Cancel"))
    button.set_action_name("app.cancel")
    style = button.get_style_context()
    style.remove_class("suggested-action")
    style.add_class("destructive-action")
    ui.set_progress_bar(0)
    ui.set_main_icon("load")
    page = ui.get("MainPage")
    page.set_description("Starting\n")
    if checks.running():
        text_stopping = _("Disconnecting…")
        page.set_title(text_stopping)
        action = "stop"
    else:
        text_starting = _("Connecting…")
        page.set_title(text_starting)
        action = "start"
        app.window.set_hide_on_close(True)
    connect(action, app)
    return None


def on_logs(*argv) -> None:
    """
    Show logs dialog
    """
    app = argv[2]
    logs_dialog = ui.get("LogsDialog")
    logs_dialog.present(app.window)


def on_clear_logs(*_) -> None:
    """
    Clear the logs
    """
    buffer = Gtk.TextBuffer()
    text_view = ui.get("LogsTextView")
    text_view.set_buffer(buffer)


def on_copy_logs(*_argv) -> None:
    """
    Copy logs to clipboard
    """
    text_view = ui.get("LogsTextView")
    buffer = text_view.get_buffer()
    text = buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter(), 0)
    clipboard = text_view.get_clipboard()
    clipboard.set(GObject.Value(str, text))
    overlay = ui.get("LogsToastOverlay")
    toast = Adw.Toast()
    toast.set_title(_("Text copied to clipboard"))
    overlay.add_toast(toast)


def on_new_id(*argv) -> None:
    """
    clicking on new id button
    """
    del argv
    if checks.running():
        tactions.new_id()
        toast = _("You have a new identity!")
    else:
        toast = _("Tractor is not running!")
    ui.notify(toast)


def on_check(*argv) -> None:
    """
    checks if tractor is connected or not
    """
    app = argv[2]
    ui.set_main_icon("load")
    button = ui.get("CheckConnectionButton")
    button.set_sensitive(False)
    task = Gio.Task.new(
        source_object=None,
        cancellable=None,
        callback=connected_checked,
        callback_data=app,
    )
    task.run_in_thread(check_connected_async)


def on_set_proxy(action, value, _app):
    """
    toggle proxy mode on system
    """
    action.set_state(value)
    db.set_val("auto-set", value)
    if checks.running():
        if value:
            proxy.proxy_set()
            toast = _("Proxy has been set")
        else:
            proxy.proxy_unset()
            toast = _("Proxy has been unset")
        ui.notify(toast)


def on_cancel(*_, **__) -> None:
    """
    abort the connection
    """
    pid = db.get_val("pid")
    os.killpg(os.getpgid(pid), signal.SIGTERM)
    dconf = config.DCONF
    dconf.reset("pid")


def open_add_bridge_dialog(*argv):
    """
    open dialog to add bridges
    """
    app = argv[2]
    dialog = ui.get("AddBridgeDialog")
    dialog.present(app.window)


def on_open_externally(*argv):
    """
    open bridges file in external text editor
    """
    app = argv[2]
    bridges_file = bridges.get_file()
    launcher = Gtk.FileLauncher()
    launcher.set_file(Gio.File.new_for_path(bridges_file))
    launcher.set_always_ask(False)
    launcher.launch(app.window, None, None, None, None)


def on_telegram(*argv) -> None:
    """
    clicking on telegram button in bridges
    """
    app = argv[2]
    launcher = Gtk.UriLauncher()
    launcher.set_uri("tg://resolve?domain=GetBridgesBot")
    launcher.launch(app.window, None, None, None, None)


def on_bridgedb(*argv) -> None:
    """
    clicking on bridgedb button in bridges
    """
    app = argv[2]
    launcher = Gtk.UriLauncher()
    launcher.set_uri("https://bridges.torproject.org/options")
    launcher.launch(app.window, None, None, None, None)


def on_email(*argv) -> None:
    """
    clicking on email button in bridges
    """
    app = argv[2]
    launcher = Gtk.UriLauncher()
    launcher.set_uri("mailto:bridges@torproject.org?body=get%20bridges")
    launcher.launch(app.window, None, None, None, None)


def stop_on_no_bridges() -> bool:
    """
    Stop connection if there is no relevant bridges
    """
    bridge_type = db.get_val("bridge-type")
    if bridge_type == "none":
        return False
    with open(bridges.get_file(), encoding="utf-8") as file:
        my_bridges = file.read()
        my_bridges = bridges.relevant_lines(my_bridges, bridge_type)
        if my_bridges:
            return False
    ui.notify(_("No relevant bridges found"))
    return True


def stop_on_no_executable() -> bool:
    """
    Stop connection if there is no relevant bridges
    """
    bridge_type = db.get_val("bridge-type")
    if bridge_type in ["none", "vanilla"]:
        return False
    transport = db.get_val("pluggable-transport")
    if transport:
        if os.path.exists(transport):
            return False
    ui.notify(_("Transport executable not found"))
    return True


def connect(action: str, app) -> None:
    """
    connect or disconnect
    """
    env = os.environ.copy()
    env["NO_COLOR"] = "1"
    task = Popen(
        [config.COMMAND, action, "--verbose"],
        stdout=PIPE,
        start_new_session=True,
        env=env,
    )
    if action == "start":
        config.DCONF.set_int("pid", task.pid)
    elif checks.proxy_set():
        proxy.proxy_unset()
    app.io_in = GLib.io_add_watch(task.stdout, GLib.IO_IN, set_progress)
    GLib.io_add_watch(task.stdout, GLib.IO_HUP, thread_finished, app)


def set_progress(stdout, _condition) -> bool:
    """
    set progress output on UI
    """
    try:
        line = stdout.readline().decode("utf-8")
    except ValueError:
        return False
    ui.add_to_terminal(line)
    if "Bootstrapped" in line:
        valid = re.compile(r".*Bootstrapped .+% \(.*\): ")
        notice = valid.sub("", line)
        ui.set_description(notice)
        percentage = line.split(" ")[5]
        ui.set_progress_bar(int(percentage[:-1]))
    elif "Process terminated" in line:
        if "Failed to bind one of the listener ports" in line:
            toast = _("One of the listener ports is in use")
        else:
            toast = line.strip()[20:]
        ui.notify(toast)
    return True


def thread_finished(stdout, condition, app) -> bool:
    """
    things to do after process finished
    """
    if condition:
        GLib.source_remove(app.io_in)
        stdout.close()
        ui.set_run_status(app)
        return False
    return True


def check_connected_async(task, *_):
    """
    wrapper to run checks.connected async
    """
    result = checks.connected()
    task.return_boolean(result)


def connected_checked(*argv) -> None:
    """
    Callback for checking connection
    """
    res, app = argv[1], argv[2]
    result = False
    if Gio.Task.is_valid(res):
        result = res.propagate_boolean()
    if result:
        toast = _("Your connection is secure")
        ui.set_main_icon("run")
    else:
        toast = _("Failed to connect to TOR network")
        ui.set_main_icon("dead")
    ui.notify(toast)
    button = ui.get("CheckConnectionButton")
    button.set_sensitive(True)
    # Show notification if window is not focused
    if app and not (app.window.is_active() and app.window.is_visible()):
        notification = Gio.Notification()
        notification.set_title(_("Connection check complete"))
        notification.set_body(toast)
        notification.set_default_action("app.show")
        if not result:
            notification.add_button(_("Try again"), "app.check-connection")
            notification.add_button(_("Quit"), "app.quit")
        app.send_notification("checked", notification)
