import io
import logging
import zipfile

from fastapi import Request
from nicegui import app, run, ui
from nicegui.events import (
    ClickEventArguments,
    GenericEventArguments,
    MultiUploadEventArguments,
)
from starlette.formparsers import MultiPartParser
from xplan_tools.interface.gml import GMLRepository
from xplan_tools.model.base import Appschema, BaseCollection

from xmas_app.components.documents import DocumentEditor
from xmas_app.pages.base import BasePage
from xmas_app.settings import get_settings
from xmas_app.util import get_model_for_select
from xmas_app.util.db import (
    get_db_plans,
    get_feature_number,
    get_plan_documents,
    save_plan_and_documents,
)

logger = logging.getLogger("xmas_app")

MultiPartParser.spool_max_size = 1024 * 1024 * 50


class PlanManagerPage(BasePage):
    """Class to encapsulate page content and methods to manage plans.

    Displays plans from the database in a table view and allow to load/download/delete them.
    Allows initialization of the digitalization of new plans in QGIS.
    Provides an Uploader to import GML instance files of supported appschemas.
    """

    def __init__(
        self,
        request: Request,
        appschema: str,
        version: str,
    ):
        """Initializes the PlanManagerPage.

        Content is rendered via the `render()` method.

        Args:
            request: a `fastapi.Request` for e.g. header extraction
            appschema: the appschema for new plans requested by the client
            version: the appschema version for new plans requested by the client
        """
        super().__init__(request)
        ui.on("planCreated", self.update_plans)
        self.appschema = appschema
        self.version = version

    async def render(self):
        """Renders the page content."""
        self.root = ui.grid(rows="auto auto auto").classes("w-full")
        with self.root:
            self.plans_dialog = ui.dialog().props("full-height")
            with ui.column(align_items="start"):
                with ui.row().classes("h-96 w-full"):
                    self.plans_table = self._create_table()
            with ui.column(align_items="start").classes("p-4 pt-2 border border-solid"):
                ui.label("Neuen Plan erstellen").classes("text-bold")
                appschema_cls = Appschema.from_prefix(self.appschema, self.version)
                self.new_plan_select = (
                    ui.select(
                        {},
                        label=f"{appschema_cls.full_name.split()[0]} {appschema_cls.version} Planart auswählen",
                    )
                    .bind_enabled_from(self, "client_is_qgis")
                    .classes("w-full")
                )
                with self.new_plan_select.add_slot("after"):
                    ui.button(
                        icon="play_circle",
                        on_click=self._on_new_plan,
                    ).bind_enabled_from(self.new_plan_select, "value").tooltip(
                        "Planerstellung starten"
                    )
            with ui.column(align_items="start").classes("relative"):
                uploader = (
                    ui.upload(
                        multiple=True,
                        max_total_size=50000000,
                        label="GML-Datei importieren",
                        on_rejected=lambda: ui.notify(
                            "Falscher Dateityp", type="negative"
                        ),
                    )
                    .props(
                        "accept='text/xml,application/gml+xml,.xml,.gml' flat square bordered"
                    )
                    .classes("w-full")
                )
                new_ids = (
                    ui.checkbox("IDs generieren")
                    .tooltip("beim Import neue Feature-IDs erzeugen")
                    .props("dark")
                    .classes("text-white text-bold absolute top-0 right-0 p-2 mr-32")
                )
                uploader.on_multi_upload(
                    lambda e: self._on_import_plans(e, new_ids.value)
                )
        try:
            await ui.context.client.connected(timeout=10)
        except TimeoutError:
            return
        await self.update_plans()
        if self.client_is_qgis:
            await get_model_for_select(
                self.new_plan_select,
                feature_regex="^.*Plan$",
                appschema=self.appschema,
                appschema_version=self.version,
            )
            self.new_plan_select.props(remove="loading")
            self.new_plan_select.set_value(self.new_plan_select.options[0])

    async def _on_import_plans(self, event: MultiUploadEventArguments, new_ids: bool):
        """Import uploaded plans to the database."""
        self.plans_table.props("loading")
        imported = []
        failed = []
        try:
            for index, content in enumerate(event.contents):
                data = io.BytesIO(content.read())
                name = event.names[index]
                try:
                    repo = GMLRepository(data)
                except Exception as e:
                    logger.exception("Uploaded file could not be read: %r", e)
                    failed.append(name)
                    continue
                try:
                    collection = await run.io_bound(repo.get_all)
                except Exception as e:
                    logger.exception("Failed to read plan from file: %r", e)
                    failed.append(name)
                try:
                    if new_ids:
                        collection = collection.make_copy()
                    await run.io_bound(get_settings().repo.save_all, collection)
                    imported.append(name)
                except Exception as e:
                    logger.exception("Failed to save plan to database: %r", e)
                    failed.append(name)
            if failed:
                ui.notify(f"Import gescheitert: {', '.join(failed)}", type="negative")
            if imported:
                ui.notify(f"Import erfolgreich: {', '.join(imported)}", type="positive")
                for client in app.clients("/plans"):
                    client.run_javascript("emitEvent('planCreated')")
        finally:
            self.plans_table.props(remove="loading")

    async def _on_load_plan(self, e: ClickEventArguments):
        """Load a plan in QGIS.

        Handles loading a selected plan and sends the plan data to the QWebChannel handler.
        """
        logger.debug("retrieving plan data to send to QGIS")
        e.sender.props(add="loading")
        try:
            plans_data = []
            for selected_plan in self.plans_table.selected:
                appschema, version = selected_plan["appschema"].split()
                plans_data.append(
                    {
                        "plan_id": selected_plan["id"],
                        "plan_name": selected_plan["name"],
                        "appschema": appschema.lower(),
                        "version": version,
                        "plan_type": selected_plan["featuretype"],
                        "bereiche": selected_plan["bereiche"],
                        "images": selected_plan["images"],
                    }
                )
            logger.debug(f"Sending plan data to QWebChannel handler: {plans_data}")
            ui.run_javascript(f"""new QWebChannel(qt.webChannelTransport, function (channel) {{
                                        channel.objects.handler.load_plan({plans_data})
                                }});""")
        finally:
            e.sender.props(remove="loading")

    async def _on_delete_plan(self, e: ClickEventArguments):
        """Delete plan using repository method from XPlan-Tools."""
        e.sender.props(add="loading")
        plans_to_delete = self.plans_table.selected
        with ui.dialog() as dialog, ui.card():
            with ui.list():
                ui.item_label("Löschen für folgende Pläne bestätigen:").props(
                    "header"
                ).classes("text-bold text-black")
                for plan in plans_to_delete:
                    with ui.item():
                        with ui.item_section().props("avatar"):
                            ui.icon("delete_forever", color="primary")
                        with ui.item_section():
                            ui.item_label(plan["name"])
                            ui.item_label(plan["id"]).props("caption")
            with ui.row().classes("w-full justify-around"):
                ui.button(icon="check", on_click=lambda: dialog.submit(True))
                ui.button(icon="close", on_click=lambda: dialog.submit(False))
        try:
            result = await dialog
            dialog.delete()
            if not result:
                return

            plan_data = []
            for plan in plans_to_delete:
                try:
                    await run.io_bound(
                        get_settings().repo.delete_plan_by_id, plan["id"]
                    )
                    logger.info("Plan %r deleted successfully", plan["id"])
                    logger.info(
                        "Sending plan delete information to QWebChannel handler"
                    )
                    plan_data.append({"id": plan["id"], "name": plan["name"]})
                except Exception:
                    logger.exception("Failed to delete plan %r", plan["id"])
                    ui.notify(
                        f"Fehler beim Löschen des Plans {plan['name']!r}.",
                        type="negative",
                    )
            ui.run_javascript(f"""new QWebChannel(qt.webChannelTransport, function (channel) {{
                                        channel.objects.handler.deleted_plans({plan_data})
                                }});""")

            for client in app.clients("/plans"):
                client.run_javascript("emitEvent('planCreated')")
        finally:
            e.sender.props(remove="loading")

    def _on_new_plan(self):
        plan_data = {
            "featuretype": self.new_plan_select.value,
        }
        ui.run_javascript(f"""new QWebChannel(qt.webChannelTransport, function (channel) {{
                                    channel.objects.handler.create_plan({plan_data})
                            }});""")

    async def _on_copy_plan(self, e: ClickEventArguments):
        """Delete plan using repository method from XPlan-Tools."""
        e.sender.props(add="loading")
        try:
            for plan in self.plans_table.selected:
                try:
                    repo = get_settings().repo
                    collection = await run.io_bound(repo.get_plan_by_id, plan["id"])
                    name = collection.features[plan["id"]].name
                    collection.features[plan["id"]].name = f"{name} Kopie"
                    collection_copy, id_map = collection.make_copy(with_id_map=True)
                    docs = get_plan_documents(plan["id"], details=True, with_data=True)
                    for doc in docs:
                        doc["id"] = id_map[plan["id"]]
                        doc.pop("pk")
                    await run.io_bound(save_plan_and_documents, collection_copy, docs)
                    logger.info("Copy of Plan %r created successfully", plan["id"])
                except Exception:
                    logger.exception("Failed to copy plan %r", plan["id"])
                    ui.notify(
                        f"Fehler beim Kopieren des Plans {plan['name']!r}.",
                        type="negative",
                    )
            await self.update_plans()
        finally:
            e.sender.props(remove="loading")

    async def _on_download_plan(self, button: ui.dropdown_button, archive=False):
        """Downloads plan as GML file or ZIP archive including plan documents."""
        button.close()
        button.props(add="loading")
        try:
            features = {}
            overall_appschema = None
            overall_version = None
            for selected_plan in self.plans_table.selected:
                try:
                    logger.debug(
                        "retreiving plan with id %r from db", selected_plan["id"]
                    )
                    appschema, version = selected_plan["appschema"].split()
                    if overall_appschema and overall_version:
                        if overall_appschema != appschema:
                            return ui.notify(
                                f"Heterogene Anwendungsschemas ({overall_appschema}, {appschema}) werden nicht unterstützt",
                                type="negative",
                            )
                        if overall_version != version:
                            return ui.notify(
                                f"Heterogene Versionen ({overall_version}, {version}) werden nicht unterstützt",
                                type="negative",
                            )
                    overall_appschema = appschema
                    overall_version = version
                    collection = await run.io_bound(
                        get_settings().repo.get_plan_by_id, selected_plan["id"]
                    )
                    srid = collection.srid
                    features.update(collection.features)
                except Exception as e:
                    logger.exception(
                        "Failed to retrieve plan %r: %r", selected_plan["id"], e
                    )
                    ui.notify(
                        f"Fehler beim Abruf von Plan {selected_plan['name']}, überspringe",
                        type="warning",
                    )
                    continue
            overall_collection = BaseCollection(
                features=features,
                srid=srid,
                version=overall_version,
                appschema=overall_appschema.lower(),
            )
            buffer = io.BytesIO()
            logger.debug("Serializing to GML via GMLRepository")
            GMLRepository(buffer).save_all(overall_collection)
            data = buffer.getvalue()
            logger.debug("Prepared download payload (%d bytes)", len(data))
            if not archive:
                return ui.download.content(
                    data,
                    filename="xplan.gml",
                    media_type="application/gml+xml",
                )
            zip_buffer = io.BytesIO()
            with zipfile.ZipFile(
                zip_buffer, mode="w", compression=zipfile.ZIP_DEFLATED
            ) as zf:
                zf.writestr(f"{overall_appschema.lower()}.gml", data)
                for selected_plan in self.plans_table.selected:
                    for doc in get_plan_documents(
                        selected_plan["id"], details=True, with_data=True
                    ):
                        zf.writestr(doc["filename"], doc["filedata"])
            return ui.download.content(
                zip_buffer.getvalue(),
                filename=f"{overall_appschema.lower()}.zip",
                media_type="application/zip",
            )

        except Exception as e:
            logger.exception("Failed to download plans: %r", e)
            ui.notify("Fehler beim Plandownload", type="negative")
        finally:
            button.props(remove="loading")

    async def update_plans(self):
        """Update plan data from database."""
        self.plans_table.props("loading")
        try:
            self.plans_table.rows.clear()
            self.plans_table.selected.clear()
            async for chunk in get_db_plans(500):
                self.plans_table.add_rows(chunk)
            self._update_row_select_label()
        except Exception as e:
            logger.exception("Fehler beim Laden der Pläne: %r", e)
            if (
                "could not connect" in str(e).lower()
                or "connection refused" in str(e).lower()
            ):
                ui.notify("Keine Verbindung zur Datenbank!", type="negative")
            else:
                ui.notify(f"Fehler beim Laden der Pläne: {e}", type="negative")
        finally:
            self.plans_table.props(remove="loading")

    def _update_row_select_label(self):
        plan_number = len(self.plans_table.rows)
        self.plans_table.props(
            f":selected-rows-label='(numberOfRows) => `${{ numberOfRows }} von {plan_number} {'Plan' if plan_number == 1 else 'Plänen'} selektiert`'"
        )

    def _create_table(self) -> ui.table:
        """Creates a table with adequate columns, props and event handlers."""
        columns = [
            {
                "name": "name",
                "label": "Name",
                "field": "name",
                "align": "left",
                "sortable": True,
                "style": "width:50%;max-width:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;",
            },
            {
                "name": "featuretype",
                "label": "Objektart",
                "field": "featuretype",
                "align": "left",
                "sortable": True,
            },
            {
                "name": "appschema",
                "label": "Appschema",
                "field": "appschema",
                "align": "left",
                "sortable": True,
            },
            {
                "name": "id",
                "label": "ID",
                "field": "id",
                "align": "left",
                "sortable": True,
                "classes": "hidden",
                "headerClasses": "hidden",
            },
            {
                "name": "updated",
                "label": "Aktualisierung",
                "field": "updated",
                "align": "left",
                "sortable": True,
                "classes": "hidden",
                "headerClasses": "hidden",
            },
            {
                "name": "created",
                "label": "Erstellung",
                "field": "created",
                "align": "left",
                "sortable": True,
                "classes": "hidden",
                "headerClasses": "hidden",
            },
            {
                "name": "docs",
                "label": "Dokumente",
                "field": "docs",
                "align": "left",
                "sortable": True,
                "classes": "hidden",
                "headerClasses": "hidden",
            },
        ]
        table = (
            ui.table(
                rows=[],
                columns=columns,
                selection="multiple",
            )
            .props(
                """
                no-data-label='keine passenden Features vorhanden'
                no-results-label='keine passenden Ergebnisse gefunden'
                flat
                square
                dense
                virtual-scroll
                bordered
            """
            )
            .classes("w-full h-full")
        )
        table.on("row-click", self._on_row_click)
        with table.add_slot("top"):
            with ui.column().classes("w-full"):
                ui.label("Vorhandene Pläne").classes("text-bold")
                with ui.row(wrap=False).classes("gap-0 w-full items-center"):
                    search = (
                        ui.input("Filter")
                        .bind_value(table, "filter")
                        .tooltip("über alle Spalten nach Ausdruck filtern")
                        .props("stack-label clearable square filled dense")
                        .classes("w-full")
                    )
                    with search.add_slot("prepend"):
                        ui.icon("filter_list")
                    ui.button(
                        icon="map", on_click=self._on_load_plan
                    ).bind_enabled_from(
                        table,
                        "selected",
                        backward=lambda selected: (
                            len(selected) > 0 and self.client_is_qgis
                        ),
                    ).tooltip("Selektion in den Layerbaum laden")
                    with (
                        ui.dropdown_button(icon="download")
                        .bind_enabled_from(
                            table,
                            "selected",
                            backward=lambda selected: len(selected) > 0,
                        )
                        .tooltip("Selektion exportieren") as download_button
                    ):
                        ui.item(
                            "GML-Datei",
                            on_click=lambda _: self._on_download_plan(download_button),
                        )
                        ui.item(
                            "ZIP-Archiv",
                            on_click=lambda _: self._on_download_plan(
                                download_button, archive=True
                            ),
                        )
                    ui.button(
                        icon="file_copy", on_click=self._on_copy_plan
                    ).bind_enabled_from(
                        table,
                        "selected",
                        backward=lambda selected: len(selected) > 0,
                    ).tooltip("Kopien von Selektion in Datenbank erzeugen")
                    ui.button(
                        icon="delete", on_click=self._on_delete_plan
                    ).bind_enabled_from(
                        table,
                        "selected",
                        backward=lambda selected: len(selected) > 0,
                    ).tooltip("Selektion aus Datenbank löschen")
                with ui.row(wrap=False).classes("gap-0 w-full items-center"):
                    ui.label("Spalten: ")
                    for column in table.columns:
                        if column["name"] in ["name", "featuretype", "appschema"]:
                            continue
                        ui.switch(
                            column["label"],
                            value=False,
                            on_change=lambda e, column=column: self._on_toggle_column(
                                column, e.value
                            ),
                        )
        return table

    def _on_toggle_column(self, column: dict, visible: bool) -> None:
        column["classes"] = "" if visible else "hidden"
        column["headerClasses"] = "" if visible else "hidden"
        self.plans_table.update()

    async def _on_row_click(self, e: GenericEventArguments):
        """Opens a dialog with action buttons for the row/feature."""
        try:
            row = e.args[1]
            feature_id = row["id"]
        except Exception as e:
            logger.exception("error on loading feature data from row: %s", e)
            return ui.notify("Fehler beim Abruf der Feature-Daten", type="negative")
        self.plans_dialog.clear()
        with self.plans_dialog, ui.card():
            ui.label("Planinformationen").classes("text-bold")
            with ui.grid(columns="auto auto").classes("items-center") as plan_info:
                ui.label("ID")
                ui.label(feature_id)
                ui.label("Name")
                ui.label(row["name"])
                ui.label("Appschema")
                ui.label(row["appschema"])
                ui.label("Aktualisierung")
                ui.label(row["updated"])
                ui.label("Erstellung/Import")
                ui.label(row["created"])
                ui.label("Bereiche")
                ui.label(len(row["bereiche"]))
                ui.label("Anzahl Features")
                spinner = ui.spinner()
            ui.label("Plandokumente").classes("text-bold")
            await DocumentEditor(plan_id=row["id"]).classes("w-full h-full").render()
        self.plans_dialog.open()
        feature_number = await run.io_bound(get_feature_number, feature_id)
        plan_info.remove(spinner)
        ui.label(str(feature_number)).move(plan_info)
