from gettext import gettext as _
from gi.repository import Adw, Gdk, Gio, GLib, Gtk

import json
import logging
import os
import re
from threading import Thread
from typing import Any, Callable, Optional

from iotas.attachment_helpers import get_attachment_disk_states
from iotas.config_manager import ConfigManager
import iotas.const as const
from iotas.markdown_helpers import parse_to_tokens
from iotas.note import Note
from iotas.sync_manager import SyncManager
from iotas.webkit_pdf_exporter import WebKitPdfExporter


@Gtk.Template(resource_path="/org/gnome/World/Iotas/ui/export_dialog.ui")
class ExportDialog(Adw.Dialog):
    __gtype_name__ = "ExportDialog"

    _main_stack: Gtk.Stack = Gtk.Template.Child()
    _headerbar_stack: Gtk.Stack = Gtk.Template.Child()
    _format_headerbar: Adw.HeaderBar = Gtk.Template.Child()
    _downloading_headerbar: Adw.HeaderBar = Gtk.Template.Child()
    _exporting_headerbar: Adw.HeaderBar = Gtk.Template.Child()
    _success_headerbar: Adw.HeaderBar = Gtk.Template.Child()
    _failure_headerbar: Adw.HeaderBar = Gtk.Template.Child()
    _failure_headerbar_label: Gtk.Label = Gtk.Template.Child()
    _format_listbox: Gtk.ListBox = Gtk.Template.Child()
    _cancel_button: Gtk.Button = Gtk.Template.Child()
    _show_button: Gtk.Button = Gtk.Template.Child()
    _downloading: Adw.StatusPage = Gtk.Template.Child()
    _exporting: Gtk.Box = Gtk.Template.Child()
    _success: Gtk.Box = Gtk.Template.Child()
    _success_label: Gtk.Label = Gtk.Template.Child()
    _failure: Gtk.Box = Gtk.Template.Child()
    _failure_label: Gtk.Label = Gtk.Template.Child()
    _download_failure: Adw.StatusPage = Gtk.Template.Child()
    _retry_row: Adw.ButtonRow = Gtk.Template.Child()

    def __init__(
        self,
        note: Note,
        html_generator,
        sync_manager: SyncManager,
        webkit_init_fn: Callable[[], None],
    ) -> None:
        super().__init__()
        self.__note = note
        self.__html_generator = html_generator
        self.__sync_manager = sync_manager
        self.__webkit_init = webkit_init_fn
        self.__config_manager = ConfigManager.get_default()

        # Any typing for lazy loading
        self.__exporter: Optional[Any] = None
        self.__show_path: Optional[str] = None
        self.__out_format: str
        self.__file_extension: str
        self.__location: Optional[str] = None
        self.__custom_formats: dict[str, dict[str, str]] = {}

        controller = Gtk.EventControllerKey.new()
        controller.connect("key-pressed", self._on_key_pressed)
        self.add_controller(controller)

        GLib.idle_add(self.__setup)

    def __setup(self) -> None:
        self._cancel_button.grab_focus()
        self.__load_extra_formats()

    @Gtk.Template.Callback()
    def _row_activated(self, _listbox: Gtk.ListBox, row: Adw.ButtonRow) -> None:
        format_id = row.get_title().lower()
        if format_id in self.__custom_formats:
            self.__file_extension = self.__custom_formats[format_id]["extension"]
            self.__out_format = self.__custom_formats[format_id]["format"]
        else:
            self.__file_extension = format_id
            self.__out_format = format_id
        self.__determine_output_location()

    @Gtk.Template.Callback()
    def _download_failure_row_activated(self, _listbox: Gtk.ListBox, row: Adw.ButtonRow) -> None:
        if row == self._retry_row:
            self.__download_missing_attachments()
        else:
            self.__export(allow_missing_images=True)

    @Gtk.Template.Callback()
    def _on_show_clicked(self, _button: Gtk.Button) -> None:
        launcher = Gtk.FileLauncher.new(Gio.File.new_for_uri(f"file://{self.__show_path}"))
        launcher.open_containing_folder()
        self.close()

    @Gtk.Template.Callback()
    def _on_cancel(self, _button: Gtk.Button) -> None:
        self.close()

    def _on_key_pressed(
        self,
        controller: Gtk.EventControllerKey,
        keyval: int,
        keycode: int,
        state: Gdk.ModifierType,
    ) -> bool:
        if self._main_stack.get_visible_child() != self._exporting:
            if keyval in (Gdk.KEY_Down, Gdk.KEY_KP_Down):
                if self.get_property("focus-widget") == self._cancel_button:
                    row = self._format_listbox.get_row_at_index(0)
                    row.grab_focus()
                    return Gdk.EVENT_STOP

        return Gdk.EVENT_PROPAGATE

    def __on_finished_downloading(self) -> None:
        self._headerbar_stack.set_visible_child(self._exporting_headerbar)
        self.__export()

    def __on_finished(self, show_path: str) -> None:
        self._main_stack.set_visible_child(self._success)
        self._headerbar_stack.set_visible_child(self._success_headerbar)
        # Translators: Description, {} is a format eg. PDF
        self._success_label.set_label(_("Exported to {}").format(self.__out_format.upper()))
        self.__show_path = show_path

        # For some reason if grab focus is called directly from the idle it blocks clean exit
        def focus_show_button():
            self._show_button.grab_focus()

        GLib.idle_add(focus_show_button)

    def __on_failed(self, reason: str) -> None:
        self._main_stack.set_visible_child(self._failure)
        self._headerbar_stack.set_visible_child(self._failure_headerbar)
        # Translators: Description, {} is a format eg. PDF
        message = _("Failed to export to {}").format(self.__out_format.upper())
        if reason != "":
            message = message + "\n\n" + reason
        logging.warning(message)
        self._failure_label.set_label(message)

    def __determine_output_location(self) -> None:
        self.__init_exporter()
        assert self.__exporter
        filesystem_writable_path = self.__get_writable_non_container_dir()
        if filesystem_writable_path:
            dialog = Gtk.FileDialog.new()
            dialog.set_initial_folder(Gio.File.new_for_path(filesystem_writable_path))
            filename = self.__exporter.build_default_filename(
                self.__note, self.__out_format, self.__file_extension
            )
            dialog.set_initial_name(filename)
            # Translators: Button
            dialog.set_accept_label(_("Export"))
            window = self.get_parent().get_root()
            dialog.save(
                window,
                None,
                self.__on_save_dialog_finish,
            )
        else:
            logging.info(
                "Can't access user documents directory, exporting to location inside container"
            )
            self.__location = None
            self.__export()

    def __on_save_dialog_finish(self, dialog: Gtk.FileDialog, task: Gio.Task) -> None:
        try:
            file = dialog.save_finish(task)
        except GLib.GError as e:
            logging.warning(f"Couldn't export note: {e.message}")
            self.close()
        else:
            self.__location = file.get_path()
            self.__save_last_export_directory()
            self.__download_missing_attachments()

    def __download_missing_attachments(self) -> None:
        _parser, tokens = parse_to_tokens(self.__note, exporting=False, tex_support=False)
        states = get_attachment_disk_states(self.__note, tokens)
        if not states.missing or not self.__sync_manager.authenticated:
            self._headerbar_stack.set_visible_child(self._exporting_headerbar)
            self.__export()
            return

        logging.info("Downloading attachments prior to export")
        self._headerbar_stack.set_visible_child(self._downloading_headerbar)
        self._main_stack.set_visible_child(self._downloading)

        def thread_do():
            if self.__sync_manager.download_attachments(states.missing, on_thread=False):
                GLib.idle_add(self.__export)
            else:
                GLib.idle_add(self.__show_attachment_download_failure)

        thread = Thread(target=thread_do)
        thread.daemon = True
        thread.start()

    def __show_attachment_download_failure(self) -> None:
        self._main_stack.set_visible_child(self._download_failure)
        self._headerbar_stack.set_visible_child(self._failure_headerbar)
        # Translators: Title
        self._failure_headerbar_label.set_label(_("Transfer Failed"))

    def __export(self, allow_missing_images: bool = False) -> None:
        assert self.__exporter

        self._main_stack.set_visible_child(self._exporting)

        self.__exporter.export(
            self.__note,
            self.__out_format,
            self.__file_extension,
            self.__config_manager.markdown_tex_support,
            allow_missing_images,
            self.__location,
        )

    def __get_writable_non_container_dir(self) -> Optional[str]:
        last_dir = self.__config_manager.last_export_directory
        if last_dir != "" and self.__dir_exists_writable(last_dir):
            return last_dir
        elif self.__can_write_documents_dir():
            return self.__get_documents_dir()
        else:
            return None

    def __get_documents_dir(self) -> Optional[str]:
        return GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_DOCUMENTS)

    def __can_write_documents_dir(self) -> bool:
        documents_dir = self.__get_documents_dir()
        if documents_dir is None:
            return False
        else:
            return self.__dir_exists_writable(documents_dir)

    def __dir_exists_writable(self, path: str) -> bool:
        return os.path.exists(path) and os.access(path, os.W_OK)

    def __init_exporter(self) -> None:
        # More lazyloading to quicken startup
        from iotas.exporter import Exporter

        self.__pdf_exporter = WebKitPdfExporter(
            self.__webkit_init(), self.__config_manager.markdown_keep_webkit_process
        )

        self.__exporter = Exporter(self.__pdf_exporter, self.__html_generator, const.PKGDATADIR)
        self.__exporter.connect("finished-downloading", lambda _o: self.__on_finished_downloading())
        self.__exporter.connect("finished", lambda _o, path: self.__on_finished(path))
        self.__exporter.connect("failed", lambda _o, reason: self.__on_failed(reason))

    def __load_extra_formats(self) -> bool:
        raw_formats = self.__config_manager.extra_export_formats
        if raw_formats.strip() == "":
            return False
        try:
            formats_json = json.loads(raw_formats)
        except json.JSONDecodeError as e:
            logging.warning(f"Failed to load extra formats: {e.msg}")
            return False

        if type(formats_json) is not list:
            logging.warning("Failed to load extra formats: JSON not being a list")
            return False

        added = False
        for item in formats_json:
            if type(item) is not dict:
                logging.warning("Failed to load extra format: item not being an object")
                continue
            if "pandocOutFormat" not in item:
                logging.warning("Failed to load extra format: missing pandocOutFormat")
                continue
            if "fileExtension" not in item:
                logging.warning("Failed to load extra format: missing fileExtension")
                continue

            out_format = item["pandocOutFormat"]
            extension = item["fileExtension"]
            if type(out_format) is not str or out_format.strip() == "":
                logging.warning("Failed to load extra format: pandocOutFormat is not a string")
                continue
            if type(extension) is not str or extension.strip() == "":
                logging.warning("Failed to load extra format: due to fileExtension is not a string")
                continue

            if re.match(r"^[\w_]+$", out_format) is None:
                logging.warning(
                    "Failed to load extra format: pandocOutFormat contains invalid characters"
                )
                continue
            if re.match(r"^[\w]+$", extension) is None:
                logging.warning(
                    "Failed to load extra format: fileExtension contains invalid characters"
                )
                continue

            logging.debug(f"Adding extra pandoc format {out_format} with extension {extension}")
            button = Adw.ButtonRow()
            button.set_title(extension.upper())
            button.set_end_icon_name("go-next-symbolic")
            self._format_listbox.append(button)

            format_id = out_format.lower()
            self.__custom_formats[format_id] = {"format": out_format, "extension": extension}

            added = True

        return added

    def __save_last_export_directory(self) -> None:
        assert self.__location  # mypy
        if os.path.isdir(self.__location):
            dir_path = os.path.abspath(os.path.join(self.__location, os.pardir))
        else:
            dir_path = os.path.dirname(self.__location)
        self.__config_manager.last_export_directory = dir_path
