# SPDX-FileCopyrightText: 2022 Chris Heywood
#
# SPDX-License-Identifier: GPL-3.0-or-later

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

import logging
import os
from tempfile import TemporaryDirectory
from typing import Callable, Optional

from iotas.attachment import Attachment
from iotas.config_manager import ConfigManager
import iotas.const as const
from iotas.export_utils import (
    build_batch_dirname,
    build_default_filename,
    get_container_export_dir,
    exporting_to_dir,
    parse_export_formats,
)
from iotas.note import Note
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()
    _checking_attachments_headerbar: Adw.HeaderBar = 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()
    _parsing: Adw.StatusPage = Gtk.Template.Child()
    _downloading: Adw.StatusPage = Gtk.Template.Child()
    _downloading_progress_bar: Gtk.ProgressBar = Gtk.Template.Child()
    _single_exporting: Gtk.Box = Gtk.Template.Child()
    _multi_exporting: Gtk.Box = Gtk.Template.Child()
    _multi_export_progress_bar: Gtk.ProgressBar = 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,
        notes: list[Note],
        html_generator,
        download_attachment: Optional[Callable[[Attachment], bool]],
        webkit_initialiser: Callable[[], None],
        note_content_populator: Optional[Callable[[Note], None]],
    ) -> None:
        super().__init__()

        self.__html_generator = html_generator
        self.__download_attachment = download_attachment
        self.__webkit_init = webkit_initialiser
        self.__note_content_populator = note_content_populator
        self.__config_manager = ConfigManager.get_default()

        # Any type for lazy loading
        self.__show_path: Optional[str] = None
        self.__custom_formats: dict[str, dict[str, str]] = {}

        self.__init_exporter()
        self.__exporter.set_notes(notes)

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

        GLib.idle_add(self.__setup)

        self.connect("closed", lambda _o: self.__on_close())

    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.__exporter.file_extension = self.__custom_formats[format_id]["extension"]
            self.__exporter.out_format = self.__custom_formats[format_id]["format"]
        else:
            self.__exporter.file_extension = format_id
            self.__exporter.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.__check_and_transfer_missing_attachments()
        else:
            self.__exporter.start_export()

    @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._single_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_note_exported(self, _obj: GObject.Object, progress: float) -> None:
        if self.__exporter.multi_export:
            self._multi_export_progress_bar.set_fraction(progress)

    def __on_batch_finished(self, _obj: GObject.Object, show_path: str) -> None:
        self._main_stack.set_visible_child(self._success)
        self._headerbar_stack.set_visible_child(self._success_headerbar)
        if self.__exporter.multi_export:
            # Translators: Description, {0} is a number greater than 1, {} is a format eg. PDF
            label = _("Exported {0} notes to {1}").format(
                len(self.__exporter.get_notes()), self.__exporter.out_format.upper()
            )
        else:
            # Translators: Description, {} is a format eg. PDF
            label = _("Exported to {}").format(self.__exporter.out_format.upper())
        self._success_label.set_label(label)
        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_export_failed(self, _obj: GObject.Object, reason: str) -> None:
        self.__report_failure(reason)

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

            # Handle case where dir actually isn't writable, possibly just a sandbox dev
            # environment issue. Early 2026.
            notes = self.__exporter.get_notes()
            if self.__exporter.multi_export or exporting_to_dir(
                self.__exporter.out_format, notes[0]
            ):
                if not self.__ensure_dir_writable():
                    return

            if self.__exporter.location:
                self.__save_last_export_directory()

            self.__start_check_or_export()

    def __on_attachment_transfer_started(self, _obj: GObject.Object, count: int) -> None:
        self._downloading_progress_bar.set_fraction(0)

        # Translators: Description, notification, {} is a number
        msg = ngettext("Transferring {} attachment", "Transferring {} attachments", count).format(
            count
        )
        self._downloading.set_description(msg)
        self._headerbar_stack.set_visible_child(self._downloading_headerbar)
        self._main_stack.set_visible_child(self._downloading)
        # self.__failed_attachment_transfers = set()

    def __on_attachment_transferred(self, _obj: GObject.Object, progress: float) -> None:
        self._downloading_progress_bar.set_fraction(progress)

    def __on_finished_attachment_run(self, _obj: GObject.Object, count: int) -> None:
        if count:
            self.__show_attachment_download_failures(count)
        else:
            self.__exporter.start_export()

    def __on_export_started(self, _obj: GObject.Object) -> None:
        self._headerbar_stack.set_visible_child(self._exporting_headerbar)
        if self.__exporter.multi_export:
            self._main_stack.set_visible_child(self._multi_exporting)
        else:
            self._main_stack.set_visible_child(self._single_exporting)

    def __on_export_progress(self, _obj: GObject.Object, progress: float) -> None:
        if self.__exporter.multi_export:
            self._multi_export_progress_bar.set_fraction(progress)

    def __on_close(self) -> None:
        if self.__exporter:
            self.__exporter.disconnect_by_func(self.__on_attachment_transfer_started)
            self.__exporter.disconnect_by_func(self.__on_attachment_transferred)
            self.__exporter.disconnect_by_func(self.__on_finished_attachment_run)
            self.__exporter.disconnect_by_func(self.__on_export_started)
            self.__exporter.disconnect_by_func(self.__on_note_exported)
            self.__exporter.disconnect_by_func(self.__on_export_failed)
            self.__exporter.disconnect_by_func(self.__on_batch_finished)
            self.__exporter.cleanup()

    def __determine_output_location(self) -> None:
        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))
            dialog.set_initial_name(self.__build_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"
            )
            path = self.__get_location_in_container()
            if path:
                self.__exporter.location = path
                self.__start_check_or_export()

    def __start_check_or_export(self) -> None:
        # If we're local only there is no attachment downloading, start the export
        if self.__download_attachment is None:
            self.__exporter.start_export()
        else:
            self.__check_and_transfer_missing_attachments()

    def __check_and_transfer_missing_attachments(self) -> None:
        self._headerbar_stack.set_visible_child(self._checking_attachments_headerbar)
        self._main_stack.set_visible_child(self._parsing)
        self.__exporter.check_and_transfer_missing_attachments()

    def __show_attachment_download_failures(self, count: int) -> None:
        # Translators: Description, notification, {} is a number
        msg = ngettext(
            "{} attachment failed to transfer", "{} attachments failed to transfer", count
        ).format(count)
        self._download_failure.set_description(msg)
        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 __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:
        if not os.path.exists(path):
            return False

        try:
            with TemporaryDirectory(dir=path):
                return True
        except OSError:
            return False

    def __get_location_in_container(self) -> Optional[str]:
        export_dir = get_container_export_dir()

        if not os.path.exists(export_dir):
            logging.debug(f"Creating missing export directory {export_dir}")
            try:
                os.mkdir(export_dir)
            except OSError as e:
                logging.warning(f"Couldn't create exports directory within container: {e}")
                self.__report_failure(str(e))
                return None

        return os.path.join(export_dir, self.__build_filename())

    def __build_filename(self) -> str:
        if self.__exporter.multi_export:
            filename = build_batch_dirname()
        else:
            note = self.__exporter.get_notes()[0]
            if not note.content_loaded:
                # Always provided when exporting from list (otherwise non-null)
                assert self.__note_content_populator
                self.__note_content_populator(note)
            filename = build_default_filename(
                note, self.__exporter.out_format, self.__exporter.file_extension
            )
        return filename

    def __init_exporter(self) -> None:
        # More lazyloading to quicken startup
        from iotas.batch_exporter import BatchExporter
        from iotas.default_note_exporter import DefaultNoteExporter

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

        single_note_exporter = DefaultNoteExporter(
            self.__pdf_exporter, self.__html_generator, const.PKGDATADIR
        )

        self.__exporter = BatchExporter(
            single_note_exporter, self.__download_attachment, self.__note_content_populator
        )
        self.__exporter.connect(
            "attachment-transfer-started", self.__on_attachment_transfer_started
        )
        self.__exporter.connect("attachment-transferred", self.__on_attachment_transferred)
        self.__exporter.connect(
            "finished-attachment-detection-transfer-run", self.__on_finished_attachment_run
        )
        self.__exporter.connect("export-started", self.__on_export_started)
        self.__exporter.connect("note-exported", self.__on_note_exported)
        self.__exporter.connect("export-failed", self.__on_export_failed)
        self.__exporter.connect("batch-finished", self.__on_batch_finished)

        self.__exporter.supporting_tex = self.__config_manager.markdown_tex_support

    def __load_extra_formats(self) -> None:
        raw_formats = self.__config_manager.extra_export_formats
        formats = parse_export_formats(raw_formats)
        if not formats:
            return

        for format in formats.values():
            button = Adw.ButtonRow()
            button.set_title(format["extension"].upper())
            button.set_end_icon_name("go-next-symbolic")
            self._format_listbox.append(button)

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

    def __ensure_dir_writable(self) -> bool:
        success = True
        if self.__exporter.multi_export:
            path = self.__exporter.location
        else:
            path = os.path.dirname(self.__exporter.location)
        if not self.__dir_exists_writable(path):
            logging.info("Can't write to selected path, changing path to within data dir/container")
            path = self.__get_location_in_container()
            if path:
                self.__exporter.location = path
            else:
                success = False
        return success

    def __report_failure(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.__exporter.out_format.upper())
        if reason != "":
            message = message + "\n\n" + reason
        logging.warning(message)
        self._failure_label.set_label(message)
