import asyncio
import logging
import re
from contextlib import asynccontextmanager
from functools import partial
from importlib import metadata
from pathlib import Path
from tempfile import gettempdir
from uuid import UUID

import pydantic
import pydantic_core
from fastapi import APIRouter, Depends, HTTPException, Request, status
from nicegui import app, run, ui
from nicegui.events import ValueChangeEventArguments
from nicegui.observables import ObservableSet
from starlette.applications import Starlette
from xplan_tools.model import model_factory

from xmas_app.components.buttons import FeatureInteractionButton
from xmas_app.deps.version_guard import enforce_plugin_version
from xmas_app.form import ModelForm
from xmas_app.models.crud import InsertPayload, UpdatePayload
from xmas_app.models.split import (
    ErrorDetail,
    ErrorResponse,
    SplitPayload,
    SplitSuccess,
)
from xmas_app.pages import PlanManagerPage
from xmas_app.services import crud
from xmas_app.services.split import PlanSplitService, SplitValidationError
from xmas_app.settings import get_settings
from xmas_app.util import get_model_for_select
from xmas_app.util.db import get_nodes

__version__ = metadata.version("xmas_app")


def _resolve_log_dir() -> Path:
    if get_settings().app_mode == "prod":
        return Path(gettempdir()) / "xmas_log"
    else:
        # dev: local repo logs
        return Path(__file__).parent.parent


log_dir = _resolve_log_dir()
log_dir.mkdir(exist_ok=True)
log_file = log_dir / "xmas_app.log"

logger = logging.getLogger("xmas_app")
logger.propagate = False

logger.handlers.clear()
logger.setLevel(logging.DEBUG if get_settings().debug else logging.INFO)
fh = logging.FileHandler(log_file, mode="w", encoding="utf-8")
formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s")
fh.setFormatter(formatter)
logger.addHandler(fh)
logger.debug(f"Writing logs to {log_file}")

ui.element.default_props("dense")

router = APIRouter()


@ui.page(
    "/plan-tree/{id}",
    reconnect_timeout=5,
    dependencies=[Depends(enforce_plugin_version)],
)
async def plan_tree(
    request: Request,
    id: UUID,
):
    id = str(id)

    async def update_nodes():
        result = await run.io_bound(get_nodes, id, app.storage.client["features"])
        if result:
            nodes, *_ = result
            app.storage.client["nodes"] = nodes
            tree.update()

    ui.colors(primary="rgb(157 157 156)", secondary="rgb(57 92 127)")
    ui.button.default_props("flat square no-caps")
    ui.button_group.default_props("flat square no-caps")

    app.storage.client["user_agent"] = request.headers.get("user-agent", None)
    qgis = app.storage.client["user_agent"].startswith(get_settings().qgis_plugin_name)
    if qgis:
        ui.add_head_html('<script src="qrc:///qtwebchannel/qwebchannel.js"></script>')

    header = ui.header().classes("bg-white text-black")
    with header:
        ui.label("Feature-Baum").classes("text-h6")
        tree_filter = (
            ui.input("Baum filtern")
            .props("clearable square filled dense")
            .classes("w-full")
        )
        with tree_filter.add_slot("after"):
            ui.button(icon="refresh", on_click=update_nodes).tooltip(
                "Baum aktualisieren"
            )
        ui.separator()
    action_dialog = ui.dialog().on("hide", lambda e: e.sender.clear())

    # --- Create the dialog placeholder once ---
    with ui.dialog() as confirm_deleteObject_dialog, ui.card(align_items="stretch"):
        label = ui.label(
            "Soll das Objekt wirklich gelöscht werden?"
        )  # placeholder text we can update dynamically

        confirm_button = ui.button("Löschen", color="red")
        ui.button("Abbrechen", on_click=confirm_deleteObject_dialog.close)

    # --- Function to open the dialog dynamically ---
    def confirm_delete(target):
        label.text = "Soll das Objekt wirklich gelöscht werden?"
        confirm_button.on("click", partial(delete_feature, target))
        confirm_deleteObject_dialog.open()

    async def show_menu(e: ValueChangeEventArguments):
        action_dialog.clear()
        if e.value:
            path = e.value.split(".")
            if len(path) == 5:
                _, origin, _, target_featuretype, target = path
                feature = get_settings().repo.get(target)
                with action_dialog, ui.card():
                    ui.label(f"{target_featuretype} {target[:8]}").classes("text-bold")
                    FeatureInteractionButton(
                        "highlight_feature",
                        target,
                        geom=feature.get_geom_wkt() is not None,
                    )
                    FeatureInteractionButton("select_feature", target)
                    FeatureInteractionButton("show_attribute_form", target)
                    if not re.match("^.._Bereich$", target_featuretype):
                        ui.button(
                            "Feature löschen",
                            on_click=lambda _, target=target: confirm_delete(target),
                            icon="delete",
                        )
            elif len(path) == 3:
                origin_featuretype, origin_id, rel = path
                with action_dialog, ui.card(align_items="stretch"):
                    ui.label(origin_featuretype + " " + origin_id[:8]).classes(
                        "text-bold"
                    )
                    if rel == "bereich":
                        ui.label(f"Keine Aktionen für Referenz {repr(rel)} verfügbar")
                    else:
                        ui.label(f"Neues Feature für Referenz {repr(rel)} erstellen")
                        feature = get_settings().repo.get(origin_id)
                        prop_info = feature.get_property_info(rel)
                        typename = prop_info["typename"]
                        targets = typename if isinstance(typename, list) else [typename]
                        target_select = ui.select(
                            targets,
                            label="Featuretype auswählen",
                            value=None,
                            with_input=True,
                        )
                        if len(targets) == 1:
                            target_select.props("readonly")
                        with target_select.add_slot("append"):
                            ui.badge(str(len(targets))).props("floating")
                        with target_select.add_slot("after"):
                            dropdown = ui.dropdown_button(
                                icon="play_circle",
                                split=True,
                            )
                            target_select.on_value_change(
                                lambda e,
                                origin_featuretype=origin_featuretype,
                                origin_id=origin_id,
                                rel=rel: prepare_add_feature(
                                    dropdown,
                                    origin_featuretype,
                                    origin_id,
                                    rel,
                                    e.value,
                                )
                            )
                            if len(targets) == 1:
                                target_select.set_value(targets[0])
                            else:
                                dropdown.props("disable")

            else:
                return
            action_dialog.open()

    async def prepare_add_feature(
        dropdown: ui.dropdown_button,
        origin_featuretype: str,
        origin_id: str,
        rel: str,
        target_featuretype: str,
    ):
        def trigger_add_feature(geometry_type):
            rel_inv = origin_feature.get_property_info(rel)["assoc_info"]["reverse"]
            data = {
                "origin_featuretype": origin_featuretype,
                "origin_id": origin_id,
                "rel": rel,
                "rel_list": origin_feature.get_property_info(rel)["list"],
                "rel_inv": rel_inv,
                "rel_inv_list": target_model.get_property_info(rel_inv)["list"]
                if rel_inv
                else False,
                "target_featuretype": target_featuretype,
                "target_geometrytype": geometry_type,
            }
            try:
                logger.info("Sending add_feature to QWebChannel handler")
                ui.run_javascript(f"""new QWebChannel(qt.webChannelTransport, function (channel) {{
                                            channel.objects.handler.add_feature({data})
                                    }});""")
            except Exception as ex:
                logger.error(f"Exception while add_feature called: {ex}", exc_info=True)

        GEOMETRY_MAP = {
            "Polygon": {"label": "Fläche", "icon": "o_space_dashboard"},
            "Line": {"label": "Linie", "icon": "show_chart"},
            "Point": {"label": "Punkt", "icon": "o_location_on"},
            "Null": {"label": "Keine Geometrie", "icon": "o_text_snippet"},
        }

        logger.debug("add_feature called")
        dropdown.clear()
        dropdown.props(remove="disable")
        origin_feature = get_settings().repo.get(origin_id)
        target_model = model_factory(
            target_featuretype,
            origin_feature.get_version(),
            origin_feature.get_appschema(),
        )
        geometry_types = []
        if geom_types := target_model.get_geom_types():
            for geom_type in geom_types:
                type_name = geom_type.get_name().replace("Multi", "")
                if type_name not in geometry_types:
                    geometry_types.append(type_name)
        else:
            geometry_types.append("Null")
        if len(geometry_types) == 1:
            dropdown.on_click(lambda: trigger_add_feature(geometry_types[0]))
            dropdown.props("disable-dropdown")
        else:
            with dropdown:
                dropdown.on_click(lambda e: e.sender.open())
                dropdown.props(remove="disable-dropdown")
                for geometry_type in geometry_types:
                    with ui.item(
                        on_click=lambda geometry_type=geometry_type: trigger_add_feature(
                            geometry_type
                        ),
                    ):
                        with ui.item_section().props("avatar"):
                            ui.icon(GEOMETRY_MAP[geometry_type]["icon"])
                        with ui.item_section():
                            ui.item_label(GEOMETRY_MAP[geometry_type]["label"])

    async def delete_feature(id):
        # TODO: Is cascading deletion desired?
        try:
            get_settings().repo.delete(id)
            ui.notify(f"Feature mit ID '{id}' wurde gelöscht", type="positive")
            await update_nodes()
            confirm_deleteObject_dialog.close()

        except Exception as e:
            print(e)
            ui.notify(f"Fehler beim Löschen vom Feature mit ID '{id}'", type="negative")

    def update_notification(e):
        notification.message = f"{len(e.sender)} Features geladen"

    await ui.context.client.connected()
    logger.debug("Feature called by ID:")
    logger.debug(id)

    notification = ui.notification(
        "0 Features geladen",
        type="ongoing",
        spinner=True,
        timeout=None,
    )

    app.storage.client["features"] = ObservableSet(on_change=update_notification)

    result = await run.io_bound(get_nodes, id, app.storage.client["features"])
    if result:
        nodes, featuretype, id, appschema = result
    else:
        return
    app.storage.client["nodes"] = nodes
    tree = ui.tree(
        app.storage.client["nodes"],
        on_select=show_menu,
        tick_strategy=None,
    ).props("accordion no-transition")

    notification.spinner = False
    notification.type = "positive"
    notification.message = "Ladevorgang abgeschlossen"
    await asyncio.sleep(1)
    notification.dismiss()

    if appschema == "xplan":
        tree.expand([id, f"{featuretype}.{id}.bereich"])
    tree_filter.bind_value_to(tree, "filter")


@ui.page("/plans", dependencies=[Depends(enforce_plugin_version)])
async def plans(
    request: Request,
    appschema: str = get_settings().appschema,
    version: str = get_settings().appschema_version,
):
    """Render the PlanManager page.

    Args:
        request: Incoming FastAPI HTTP request to check the user-agent header.
        appschema (str): Application schema name (default from settings).
        version (str): Schema version (default from settings).
    """
    await PlanManagerPage(
        request=request, appschema=appschema, version=version
    ).render()


@ui.page("/feature/{id}", dependencies=[Depends(enforce_plugin_version)])
async def feature(
    request: Request,
    id: UUID,
    planId: UUID | None = None,
    parentId: UUID | None = None,
    featureType: str | None = None,
    featuretypeRegex: str | None = None,
    appschema: str = get_settings().appschema,
    version: str = get_settings().appschema_version,
):
    """
    Render and manage a single feature view for the XPlan-GUI application.

    Args:
    request (Request): The incoming FastAPI HTTP request object.
    id (str): UUID or identifier of the feature to display/edit.
    planId (str | None): Optional ID of the associated plan.
    parentId (str | None): Optional ID of the parent feature.
    featureType (str | None): Optional fixed feature type to display.
    featuretypeRegex (str | None): Optional regex to filter allowed feature types.
    appschema (str): The application schema name (defaults from settings).
    version (str): Schema version string (defaults from settings).

    """
    # async def handle_properties_received(event) -> None:
    #     form = app.storage.client["form"]
    #     # wait for form to initialize/let user choose featuretype
    #     while not getattr(form, "feature", None):
    #         await asyncio.sleep(0.5)
    #         # break endless loop if client disconnected (i.e., closed attribute form)
    #         if ui.context.client.id not in ui.context.client.instances.keys():
    #             return
    #     data = form.feature | event.args
    #     geom = data.pop("geometry", None)
    #     if model_geom := form.model.get_geom_field():
    #         data[model_geom] = geom
    #     form.feature.update(data)
    #     form._get_art_options()

    id = str(id)

    async def get_qgis_feature():
        return await ui.run_javascript(
            """return await new Promise((resolve, reject) => {
                    new QWebChannel(qt.webChannelTransport, function (channel) {
                        channel.objects.handler.transfer_feature().then(data => data ? resolve(data) : reject())
                    })
                });""",
            timeout=3,
        )

    async def add_form(
        feature_type: str,
        feature: dict,
    ) -> None:
        content.set_visibility(False)
        spinner.set_visibility(True)
        with content:
            await asyncio.sleep(0.1)  # let spinner transfer state to client
            await app.storage.client["form"].render_form(feature_type, feature)
            if get_settings().debug:
                with ui.row().classes("w-full"):
                    obj_log = ui.log()
                    ui.button(
                        "show model",
                        on_click=lambda: (
                            obj_log.clear(),
                            obj_log.push(
                                model.model_dump_json(indent=2)
                                if (model := app.storage.client["form"].model_instance)
                                else "No Model",
                            ),
                        ),
                    )
                    ui.button(
                        "show submodels",
                        on_click=lambda: (
                            obj_log.clear(),
                            obj_log.push(
                                "\n".join(
                                    model.model_dump_json()
                                    for model in app.storage.client[
                                        "form"
                                    ].sub_models.values()
                                )
                            ),
                        ),
                    )
        spinner.delete()
        content.set_visibility(True)

    def handle_save():
        form: ModelForm = app.storage.client["form"]
        feature = form.model_instance
        if feature:
            feature.id = id
            get_settings().repo.update(feature)
            ui.navigate.to(f"/?saved_feature_uuid={feature.id}")

    def handle_delete():
        form: ModelForm = app.storage.client["form"]
        feature = form.model_instance
        if feature:
            get_settings().repo.delete(id)
            ui.navigate.to("/")

    def on_edit_mode_changed(e):
        if form := app.storage.client["form"]:
            form.editable = e.args

    ui.colors(primary="rgb(157 157 156)", secondary="rgb(57 92 127)")
    app.storage.client["form"] = None
    app.storage.client["plan_id"] = str(planId) if planId else None
    app.storage.client["parent_id"] = str(parentId) if parentId else None
    app.storage.client["user_agent"] = request.headers.get("user-agent", None)

    qgis = app.storage.client["user_agent"].startswith(get_settings().qgis_plugin_name)

    if qgis:
        ui.add_head_html('<script src="qrc:///qtwebchannel/qwebchannel.js"></script>')

    try:
        model = get_settings().repo.get(id)
        feature_type = model.get_name()
        feature = model.model_dump(
            mode="json", context={"datatype": True, "file_uri": True}
        )
    except ValueError:
        feature = {"id": id}
        feature_type = featureType

    ui.on("editModeChanged", on_edit_mode_changed)

    header = ui.header()  # .classes("bg-white text-black")

    if not qgis:
        with header:
            with ui.column(align_items="center").bind_visibility_from(
                app.storage.client,
                "form",
                backward=lambda form: getattr(form, "rendered", False),
            ):
                with ui.row(align_items="center"):
                    ui.button(
                        text="Speichern",
                        icon="save",
                        on_click=handle_save,
                    ).bind_enabled_from(app.storage.client, "form").props(
                        "unelevated rounded no-caps"
                    )
                    ui.button(
                        text="Löschen",
                        icon="delete",
                        on_click=handle_delete,
                    ).bind_enabled_from(app.storage.client, "form").props(
                        "unelevated rounded no-caps"
                    )
                    ui.button(
                        text="Abbrechen",
                        icon="cancel",
                        on_click=lambda: ui.navigate.to("/"),
                    ).props("unelevated rounded no-caps")
    spinner = ui.spinner(size="xl").classes("absolute-center")
    with ui.grid(columns=2 if get_settings().debug else 1).classes(
        "justify-center items-start"
    ) as content:
        with ui.column(align_items="center").classes(
            "justify-center"
        ):  # .classes("absolute-center"):
            with ui.row(align_items="center").classes("col-grow") as sel:
                ui.label("Objektart auswählen").classes("text-h6")
                model_select = (
                    ui.select(
                        options=[],
                        value=None,
                        label="Objektarten",
                        on_change=lambda x: add_form(x.value, feature),
                        with_input=True,
                    )
                    .props("square filled options-dense")
                    .style("width: 500px;")
                )
                # .classes("absolute-center")
            if feature_type:
                sel.set_visibility(False)
            else:
                spinner.set_visibility(False)
        app.storage.client["form"] = ModelForm(appschema, version, content, header)
        await ui.context.client.connected(
            timeout=10
        )  # see https://nicegui.io/documentation/page#wait_for_client_connection
        if qgis:
            qgis_feature = await get_qgis_feature()
            if isinstance(properties := qgis_feature.get("properties"), dict):
                feature = feature | properties
            if isinstance(geometry := qgis_feature.get("geometry"), dict):
                feature["geometry"] = geometry
        if feature_type:
            await add_form(feature_type, feature)
        else:
            await get_model_for_select(
                model_select,
                qgis_feature.get("geometry", {}).get("wkt", None) if qgis else None,
                featuretypeRegex,
                appschema,
                version,
            )


@app.get("/health_check")
def health_check() -> dict:
    """Health check endpoint to verify that the service is running.

    Returns:
        dict: A simple dictionary with the app's name and version.
    """
    return {"name": "XMAS-App", "version": __version__}


@app.post(
    "/insert-features", status_code=201, dependencies=[Depends(enforce_plugin_version)]
)
async def insert_features(payload: InsertPayload, planId: UUID):
    """Insert a number of features."""
    await crud.create(payload, str(planId))


@app.post(
    "/update-features", status_code=204, dependencies=[Depends(enforce_plugin_version)]
)
async def update_features(payload: UpdatePayload):
    """Update a number of features."""
    await crud.update(payload)


@app.post(
    "/split-tool",
    response_model=SplitSuccess,
    status_code=status.HTTP_201_CREATED,  # semantically 'created'
    responses={
        400: {"model": ErrorResponse, "description": "Bad Request"},
        422: {"model": ErrorResponse, "description": "Validation Error"},
        500: {"model": ErrorResponse, "description": "Server Error"},
    },
    dependencies=[Depends(enforce_plugin_version)],
)
async def receive_split_plans(payload: SplitPayload) -> SplitSuccess:
    logger.info("Split plans endpoint reached.")

    API_VERSION = "split-import-v1-min"

    # Lightweight contract check up front
    if payload.schema_version != API_VERSION:
        logger.error("Payload api schema version incorrect.")
        raise HTTPException(
            status_code=400,
            detail=ErrorDetail(
                code="invalid_schema_version", message="Invalid schema_version"
            ).model_dump(),
        )

    # Early existence check to short-circuit before the service
    try:
        source_plan = get_settings().repo.get_plan_by_id(payload.src_plan_id)
        if source_plan is None:
            logger.error("Couldn't load plan from database with provided id.")
            raise HTTPException(
                status_code=400,
                detail=ErrorDetail(
                    code="plan_not_found", message="Plan not found"
                ).model_dump(),
            )
    except (pydantic.ValidationError, pydantic_core.ValidationError) as e:
        logger.error(f"Plan validation failed: {e}")
        raise HTTPException(
            status_code=422,
            detail=ErrorDetail(code="validation_error", message=str(e)).model_dump(),
        )

    service = PlanSplitService(get_settings().repo)

    try:
        return service.apply_split_import(payload, source_plan)
    except SplitValidationError as e:
        logger.warning("Split validation failed with %d violations", len(e.violations))
        raise HTTPException(
            status_code=400,
            detail=ErrorDetail(
                code="validation_failed",
                message=e.message,
                violations=[
                    {
                        "code": v.code,
                        "featuretype": v.featuretype,
                        "old_id": v.old_id,
                        "hint": v.hint,
                    }
                    for v in e.violations
                ],
            ).model_dump(),
        )
    except ValueError as e:
        logger.error("Domain error during split: %s", e)
        raise HTTPException(
            status_code=400,
            detail=ErrorDetail(code="domain_error", message=str(e)).model_dump(),
        )
    except Exception as e:
        logger.exception(
            "Unhandled split processing error. "
            "Likely dangling association reference. "
            "Check for edges whose dst_old not in id_map."
        )
        raise HTTPException(
            status_code=500,
            detail=ErrorDetail(
                code="server_error", message=f"Processing error: {e}"
            ).model_dump(),
        ) from e


@asynccontextmanager
async def lifespan(starlette_app):
    msg = f"XMAS-App is running on port {get_settings().app_port} in {get_settings().app_mode} mode."
    logger.info(msg)
    print(msg)
    yield
    logger.info("XMAS-App is shutting down.")
    print("XMAS-App is shutting down.")


starlette_app = Starlette(
    debug=get_settings().debug,
    routes=None,
    lifespan=lifespan,
)
ui.run_with(starlette_app)


def create_app() -> Starlette:
    """Factory for the plugin: return the ASGI app, without asyncio or uvicorn."""

    # os.environ["PYGEOAPI_CONFIG"] = str(Path(__file__).parent / "pygeoapi" / "config.yaml")
    # os.environ["PYGEOAPI_OPENAPI"] = str(
    #     Path(__file__).parent / "pygeoapi" / "openapi.yaml"
    # )
    # from pygeoapi.starlette_app import APP as pygeoapi_app

    # starlette.mount("/oapi", pygeoapi_app)

    return starlette_app


def run_server():
    import uvicorn

    print("Starting server in stand alone mode.")
    logger.info("Starting server in stand alone mode.")

    try:
        uvicorn.run(
            "xmas_app.main:starlette_app",
            host="0.0.0.0",
            port=get_settings().app_port,
            reload=get_settings().app_mode == "dev",
            log_config=None,  # avoids isatty() issue
        )
    except Exception as e:
        msg = f"Failed to start server: {e}"
        print(msg)
        logger.error(msg)


if __name__ == "__main__":
    run_server()
