# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html

from __future__ import annotations

import os
import re
import time
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Sequence, Type

import aqt.forms
import aqt.main
from anki.collection import DeckIdLimit, ExportLimit, NoteIdsLimit, Progress
from anki.decks import DeckId, DeckNameId
from anki.notes import NoteId
from aqt import gui_hooks
from aqt.errors import show_exception
from aqt.operations import QueryOp
from aqt.progress import ProgressUpdate
from aqt.qt import *
from aqt.utils import (
    checkInvalidFilename,
    disable_help_button,
    getSaveFile,
    showWarning,
    tooltip,
    tr,
)


class ExportDialog(QDialog):
    def __init__(
        self,
        mw: aqt.main.AnkiQt,
        did: DeckId | None = None,
        nids: Sequence[NoteId] | None = None,
    ):
        QDialog.__init__(self, mw, Qt.WindowType.Window)
        self.mw = mw
        self.col = mw.col.weakref()
        self.frm = aqt.forms.exporting.Ui_ExportDialog()
        self.frm.setupUi(self)
        self.exporter: Exporter
        self.nids = nids
        disable_help_button(self)
        self.setup(did)
        self.open()

    def setup(self, did: DeckId | None) -> None:
        self.exporter_classes: list[Type[Exporter]] = [
            ApkgExporter,
            ColpkgExporter,
            NoteCsvExporter,
            CardCsvExporter,
        ]
        gui_hooks.exporters_list_did_initialize(self.exporter_classes)
        self.frm.format.insertItems(
            0, [f"{e.name()} (.{e.extension})" for e in self.exporter_classes]
        )
        qconnect(self.frm.format.activated, self.exporter_changed)
        if self.nids is None and not did:
            # file>export defaults to colpkg
            default_exporter_idx = 1
        else:
            default_exporter_idx = 0
        self.frm.format.setCurrentIndex(default_exporter_idx)
        self.exporter_changed(default_exporter_idx)
        # deck list
        if self.nids is None:
            self.all_decks = self.col.decks.all_names_and_ids()
            decks = [tr.exporting_all_decks()]
            decks.extend(d.name for d in self.all_decks)
        else:
            decks = [tr.exporting_selected_notes()]
        self.frm.deck.addItems(decks)
        # save button
        b = QPushButton(tr.exporting_export())
        self.frm.buttonBox.addButton(b, QDialogButtonBox.ButtonRole.AcceptRole)
        self.frm.includeHTML.setChecked(True)
        # set default option if accessed through deck button
        if did:
            name = self.mw.col.decks.get(did)["name"]
            index = self.frm.deck.findText(name)
            self.frm.deck.setCurrentIndex(index)
            self.frm.includeSched.setChecked(False)

    def exporter_changed(self, idx: int) -> None:
        self.exporter = self.exporter_classes[idx]()
        self.frm.includeSched.setVisible(self.exporter.show_include_scheduling)
        self.frm.includeMedia.setVisible(self.exporter.show_include_media)
        self.frm.includeTags.setVisible(self.exporter.show_include_tags)
        self.frm.includeHTML.setVisible(self.exporter.show_include_html)
        self.frm.includeDeck.setVisible(self.exporter.show_include_deck)
        self.frm.includeNotetype.setVisible(self.exporter.show_include_notetype)
        self.frm.includeGuid.setVisible(self.exporter.show_include_guid)
        self.frm.legacy_support.setVisible(self.exporter.show_legacy_support)
        self.frm.deck.setVisible(self.exporter.show_deck_list)

    def accept(self) -> None:
        if not (out_path := self.get_out_path()):
            return
        self.exporter.export(self.mw, self.options(out_path))
        QDialog.reject(self)

    def get_out_path(self) -> str | None:
        filename = self.filename()
        while True:
            path = getSaveFile(
                parent=self,
                title=tr.actions_export(),
                dir_description="export",
                key=self.exporter.name(),
                ext="." + self.exporter.extension,
                fname=filename,
            )
            if not path:
                return None
            if checkInvalidFilename(os.path.basename(path), dirsep=False):
                continue
            path = os.path.normpath(path)
            if os.path.commonprefix([self.mw.pm.base, path]) == self.mw.pm.base:
                showWarning("Please choose a different export location.")
                continue
            break
        return path

    def options(self, out_path: str) -> ExportOptions:
        limit: ExportLimit = None
        if self.nids:
            limit = NoteIdsLimit(self.nids)
        elif current_deck_id := self.current_deck_id():
            limit = DeckIdLimit(current_deck_id)

        return ExportOptions(
            out_path=out_path,
            include_scheduling=self.frm.includeSched.isChecked(),
            include_media=self.frm.includeMedia.isChecked(),
            include_tags=self.frm.includeTags.isChecked(),
            include_html=self.frm.includeHTML.isChecked(),
            include_deck=self.frm.includeDeck.isChecked(),
            include_notetype=self.frm.includeNotetype.isChecked(),
            include_guid=self.frm.includeGuid.isChecked(),
            legacy_support=self.frm.legacy_support.isChecked(),
            limit=limit,
        )

    def current_deck_id(self) -> DeckId | None:
        return (deck := self.current_deck()) and DeckId(deck.id) or None

    def current_deck(self) -> DeckNameId | None:
        if self.exporter.show_deck_list:
            if idx := self.frm.deck.currentIndex():
                return self.all_decks[idx - 1]
        return None

    def filename(self) -> str:
        if self.exporter.show_deck_list:
            deck_name = self.frm.deck.currentText()
            stem = re.sub('[\\\\/?<>:*|"^]', "_", deck_name)
        else:
            time_str = time.strftime("%Y-%m-%d@%H-%M-%S", time.localtime(time.time()))
            stem = f"{tr.exporting_collection()}-{time_str}"
        return f"{stem}.{self.exporter.extension}"


@dataclass
class ExportOptions:
    out_path: str
    include_scheduling: bool
    include_media: bool
    include_tags: bool
    include_html: bool
    include_deck: bool
    include_notetype: bool
    include_guid: bool
    legacy_support: bool
    limit: ExportLimit


class Exporter(ABC):
    extension: str
    show_deck_list = False
    show_include_scheduling = False
    show_include_media = False
    show_include_tags = False
    show_include_html = False
    show_legacy_support = False
    show_include_deck = False
    show_include_notetype = False
    show_include_guid = False

    @abstractmethod
    def export(self, mw: aqt.main.AnkiQt, options: ExportOptions) -> None:
        pass

    @staticmethod
    @abstractmethod
    def name() -> str:
        pass


class ColpkgExporter(Exporter):
    extension = "colpkg"
    show_include_media = True
    show_legacy_support = True

    @staticmethod
    def name() -> str:
        return tr.exporting_anki_collection_package()

    def export(self, mw: aqt.main.AnkiQt, options: ExportOptions) -> None:
        options = gui_hooks.exporter_will_export(options, self)

        def on_success(_: None) -> None:
            mw.reopen()
            gui_hooks.exporter_did_export(options, self)
            tooltip(tr.exporting_collection_exported(), parent=mw)

        def on_failure(exception: Exception) -> None:
            mw.reopen()
            show_exception(parent=mw, exception=exception)

        gui_hooks.collection_will_temporarily_close(mw.col)
        QueryOp(
            parent=mw,
            op=lambda col: col.export_collection_package(
                options.out_path,
                include_media=options.include_media,
                legacy=options.legacy_support,
            ),
            success=on_success,
        ).with_backend_progress(export_progress_update).failure(
            on_failure
        ).run_in_background()


class ApkgExporter(Exporter):
    extension = "apkg"
    show_deck_list = True
    show_include_scheduling = True
    show_include_media = True
    show_legacy_support = True

    @staticmethod
    def name() -> str:
        return tr.exporting_anki_deck_package()

    def export(self, mw: aqt.main.AnkiQt, options: ExportOptions) -> None:
        options = gui_hooks.exporter_will_export(options, self)

        def on_success(count: int) -> None:
            gui_hooks.exporter_did_export(options, self)
            tooltip(tr.exporting_note_exported(count=count), parent=mw)

        QueryOp(
            parent=mw,
            op=lambda col: col.export_anki_package(
                out_path=options.out_path,
                limit=options.limit,
                with_scheduling=options.include_scheduling,
                with_media=options.include_media,
                legacy_support=options.legacy_support,
            ),
            success=on_success,
        ).with_backend_progress(export_progress_update).run_in_background()


class NoteCsvExporter(Exporter):
    extension = "txt"
    show_deck_list = True
    show_include_html = True
    show_include_tags = True
    show_include_deck = True
    show_include_notetype = True
    show_include_guid = True

    @staticmethod
    def name() -> str:
        return tr.exporting_notes_in_plain_text()

    def export(self, mw: aqt.main.AnkiQt, options: ExportOptions) -> None:
        options = gui_hooks.exporter_will_export(options, self)

        def on_success(count: int) -> None:
            gui_hooks.exporter_did_export(options, self)
            tooltip(tr.exporting_note_exported(count=count), parent=mw)

        QueryOp(
            parent=mw,
            op=lambda col: col.export_note_csv(
                out_path=options.out_path,
                limit=options.limit,
                with_html=options.include_html,
                with_tags=options.include_tags,
                with_deck=options.include_deck,
                with_notetype=options.include_notetype,
                with_guid=options.include_guid,
            ),
            success=on_success,
        ).with_backend_progress(export_progress_update).run_in_background()


class CardCsvExporter(Exporter):
    extension = "txt"
    show_deck_list = True
    show_include_html = True

    @staticmethod
    def name() -> str:
        return tr.exporting_cards_in_plain_text()

    def export(self, mw: aqt.main.AnkiQt, options: ExportOptions) -> None:
        options = gui_hooks.exporter_will_export(options, self)

        def on_success(count: int) -> None:
            gui_hooks.exporter_did_export(options, self)
            tooltip(tr.exporting_card_exported(count=count), parent=mw)

        QueryOp(
            parent=mw,
            op=lambda col: col.export_card_csv(
                out_path=options.out_path,
                limit=options.limit,
                with_html=options.include_html,
            ),
            success=on_success,
        ).with_backend_progress(export_progress_update).run_in_background()


def export_progress_update(progress: Progress, update: ProgressUpdate) -> None:
    if not progress.HasField("exporting"):
        return
    update.label = progress.exporting
    if update.user_wants_abort:
        update.abort = True
