"""Canopy Viewer - NiceGUI app for creating canopy figures from JSON config."""
import argparse
import os
import sys
import tempfile
import traceback
from pathlib import Path

from nicegui import app, run, ui

# Raise binding propagation threshold to reduce log warnings (default 0.01 s)
import nicegui.binding
nicegui.binding.MAX_PROPAGATION_TIME = 0.05

# Serve logo from _static folder (in canopy_viewer package when installed)
import canopy_viewer
app.add_static_files("/static", Path(canopy_viewer.__file__).parent / "_static")

from components.field_selector import field_selector
from components.local_file_picker import LocalSaveDialog
from components.options_panel import (
    options_panel,
    is_figure_enabled,
    get_enabled_figures,
    resolve_selected_figure,
)
from utils.json_builder import build_and_save_config


# -----------------------------------------------------------------------------
# Entry point
# -----------------------------------------------------------------------------

@ui.page("/", dark=True)
def index() -> None:
    create_content()


def main() -> None:
    """Launch the Canopy Viewer app."""
    parser = argparse.ArgumentParser(description="Canopy Viewer")
    parser.add_argument(
        "--browser",
        action="store_true",
        help="Run in browser instead of native window (use when GTK/Qt are not available, e.g. on Linux)",
    )
    args, _ = parser.parse_known_args()
    browser = args.browser or os.environ.get("CANOPY_VIEWER_BROWSER", "").lower() in ("1", "true", "yes")
    native = not browser
    ui.run(
        title="Canopy Viewer",
        native=native,
        window_size=(1920, 1080) if native else None,
        reload=False,
    )


def run_app() -> None:
    """Entry point for the canopy-viewer script."""
    main()


# -----------------------------------------------------------------------------
# Main UI
# -----------------------------------------------------------------------------

def create_content() -> None:
    """Build the main UI."""
    fields: list[str] = []
    state: dict = _get_initial_state()
    selected_figure: str = ""
    # Refs passed to options_panel; avoids NiceGUI destroying inputs when visibility toggles
    toggle_ref: dict = {}
    sub_panels: dict = {}
    previous_temp_dir: tempfile.TemporaryDirectory | None = None

    def _set_conditional_visibility(n: int, fig: str) -> None:
        """Update visibility of figure-specific conditional options."""
        if toggle_ref and "make_diff_container" in toggle_ref:
            toggle_ref["make_diff_container"].set_visibility(n == 2 and fig == "time_series")
        if toggle_ref and "field_labels_container" in toggle_ref:
            toggle_ref["field_labels_container"].set_visibility(n > 1 and fig == "time_series")
        if toggle_ref and "latitudinal_field_labels_container" in toggle_ref:
            toggle_ref["latitudinal_field_labels_container"].set_visibility(n > 1 and fig == "latitudinal_plot")
        if toggle_ref and "distribution_field_labels_container" in toggle_ref:
            toggle_ref["distribution_field_labels_container"].set_visibility(n > 1 and fig == "distribution")

    def on_figure_select(fig: str) -> None:
        nonlocal selected_figure
        selected_figure = fig
        if fig == "distribution" and state.get("gridop") is None:
            state["gridop"] = "av"
        for f, panel in sub_panels.items():
            panel.set_visibility(f == fig)
        _set_conditional_visibility(len(fields), fig)

    def update_options_visibility() -> None:
        nonlocal selected_figure
        n = len(fields)
        main_panel.set_visibility(n > 0)
        _set_conditional_visibility(n, selected_figure)
        # Sync figure-type toggle with enabled options; auto-select first if current invalid
        if "toggle" in toggle_ref:
            enabled = get_enabled_figures(n)
            val = resolve_selected_figure(selected_figure, enabled)
            if val and val != selected_figure:
                selected_figure = val
                for f, panel in sub_panels.items():
                    panel.set_visibility(f == val)
            toggle_ref["toggle"].set_options(enabled, value=val)
            toggle_ref["toggle"].update()

    def on_fields_change() -> None:
        nonlocal selected_figure
        n = len(fields)
        # Clear selection if current figure type no longer valid for new field count
        if selected_figure and not is_figure_enabled(selected_figure, n):
            selected_figure = ""
            for f, panel in sub_panels.items():
                panel.set_visibility(False)
        update_options_visibility()

    async def run_figure() -> None:
        nonlocal previous_temp_dir
        err = _validate_run_request(list(fields), selected_figure, state)
        if err:
            ui.notify(err, type="negative" if "Canopy package" in err else "warning")
            return

        # Clean up previous run's temp dir before creating a new one
        if previous_temp_dir:
            try:
                previous_temp_dir.cleanup()
            except OSError:
                pass
            previous_temp_dir = None

        tmpdir_obj = tempfile.TemporaryDirectory(prefix="canopy_viewer_")
        tmpdir = tmpdir_obj.name

        # Show loading state
        figure_display.clear()
        with figure_display:
            with ui.column().classes("items-center gap-4 py-16"):
                ui.spinner(size="lg")
                ui.label("Generating figure...").classes("text-lg text-gray-400")
        make_figure_btn.props(add="loading")

        try:
            # Run in separate process to avoid blocking event loop (prevents reconnect popups)
            result, error = await run.cpu_bound(
                _generate_figure,
                tmpdir,
                list(fields),
                selected_figure,
                dict(state),  # copy; subprocess gets its own state
            )
        finally:
            make_figure_btn.props(remove="loading")

        if error:
            try:
                tmpdir_obj.cleanup()
            except OSError:
                pass
            figure_display.clear()
            print(traceback.format_exc(), file=sys.stderr)
            ui.notify(error, type="negative")
            return

        previous_temp_dir = tmpdir_obj
        output_path, json_path = result
        try:
            figure_display.clear()
            with figure_display:
                ui.image(output_path).classes("max-w-full rounded-xl")

                async def save_file(path: str, default_name: str) -> None:
                    dialog = LocalSaveDialog(path, default_filename=default_name)
                    result_path = await dialog
                    if result_path:
                        ui.notify(f"Saved to {result_path}", type="positive")

                with ui.row().classes("mt-2 gap-2"):
                    ui.button(
                        "Save image",
                        on_click=lambda: save_file(output_path, "canopy_figure.png"),
                    ).props("flat size=sm icon=save")
                    ui.button(
                        "Save JSON",
                        on_click=lambda: save_file(json_path, "canopy_config.json"),
                    ).props("flat size=sm icon=code")
            ui.notify("Figure created successfully.", type="positive")
        except RuntimeError as e:
            if "deleted" not in str(e).lower():
                raise
            # Client/page was closed during figure generation; nothing to update

    _add_dark_mode_styles()

    with ui.row().classes("w-full h-[calc(100vh-2rem)] min-h-0 items-stretch"):
        # Sidebar: field selector + figure options + run button
        with ui.column().classes(
            "!bg-[#2F2F2F] border-r border-white/1 text-gray-100 w-100 p-4 gap-4 h-full min-h-0 flex flex-col rounded-l-xl rounded-r-xl"
        ):
            ui.image("/static/logo.png").classes("w-full max-w-[280px] mx-auto mb-0 object-contain")
            with ui.row().classes("w-full justify-center gap-2 mt-0 mb-0"):
                ui.link("Documentation website", "https://canopy-tools.readthedocs.io/", new_tab=True).classes(
                    "px-2 py-0.5 rounded text-white no-underline text-sm"
                ).style("background-color: #4FCA21")
                ui.link("Gallery website", "https://canopy.imk-ifu.kit.edu/", new_tab=True).classes(
                    "px-2 py-0.5 rounded text-white no-underline text-sm"
                ).style("background-color: #1A79CE")
            with ui.scroll_area().classes("flex-1 min-h-0 min-h-[300px] w-full"):
                with ui.column().classes("w-full gap-4"):
                    ui.label("Fields").classes("text-base font-semibold")
                    field_selector(fields, on_change=on_fields_change)

                    ui.separator()
                    with ui.column().classes("w-full"):
                        main_panel = ui.column().classes("w-full")
                        with main_panel:  # figure-type options; hidden until fields selected
                            options_panel(
                                main_panel,
                                state,
                                len(fields),
                                selected_figure,
                                on_figure_select,
                                toggle_ref=toggle_ref,
                                sub_panels=sub_panels,
                            )
                        main_panel.set_visibility(False)
                        update_options_visibility()

            make_figure_btn = ui.button("Make figure", on_click=run_figure).props(
                "color=positive size=lg rounded"
            ).classes("w-full")

        # Content area: rendered figure output (pt-0 aligns top with sidebar)
        with ui.column().classes("flex-1 min-w-0 px-4 pb-4 pt-0 overflow-auto"):
            figure_display = ui.column().classes("max-w-4xl w-full items-center")


# -----------------------------------------------------------------------------
# Helpers
# -----------------------------------------------------------------------------

def _get_initial_state() -> dict:
    """Return default state for figure options."""
    return {
        "grid_type": "lonlat",
        "file_format": "",
        "source": "",
        "time_slice_start": None,
        "time_slice_end": None,
        "lat_slice_min": None,
        "lat_slice_max": None,
        "lon_slice_min": None,
        "lon_slice_max": None,
        "region": "",
        "region_type": "country",
        "convert_units": "",
        "filter": "",
        "apply_op": "",
        "apply_operand": None,
        "apply_layers": "",
        "unite": False,
        "custom_palette": "",
        "title": "",
        "palette": "",
        "dark_mode": False,
        "transparent": False,
        "x_fig": 10,
        "y_fig": 10,
        "layer": "",
        "timeop": "av",
        "cb_label": "",
        "unit": "",
        "n_classes": 4,
        "classification": "linear",
        "orientation": "horizontal",
        "extend": "neither",
        "proj": "Robinson",
        "force_zero": False,
        "stats_annotation": False,
        "percentage": False,
        "nonsig": False,
        "layers": "",
        "gridop": None,
        "make_diff": False,
        "yaxis_label": "",
        "field_labels": "",
        "move_legend": False,
        "legend_style": "default",
        "reverse_hue_style": False,
        "max_labels_per_col": 15,
        "baseline": False,
        "rolling_size": None,
        "stacked": False,
        "relative": False,
        "yaxis_lim": "",
        "field_a_label": "",
        "field_b_label": "",
        "unit_a": "",
        "unit_b": "",
        "scatter_size": 6,
        "scatter_alpha": 0.1,
        "plot_type": "box",
        "horizontal": False,
        "vertical_xlabels": False,
    }


def _add_dark_mode_styles() -> None:
    """Add dark mode styles and prevent Dark Reader extension from overriding."""
    ui.add_head_html("""
        <meta name="darkreader-lock">
        <style>
            html, body { height: 100%; min-height: 100%; }
            body, .nicegui-content { background-color: #1F1F1F !important; height: 100% !important; }
        </style>
    """)


def _validate_run_request(
    fields: list[str], selected_figure: str, state: dict
) -> str | None:
    """Validate run request. Returns error message if invalid, None if ok."""
    if not fields:
        return "Please select at least one field."
    if not selected_figure or not is_figure_enabled(selected_figure, len(fields)):
        return "Please select a valid figure type."
    if selected_figure in ("map_simple", "map_diff") and not state.get("layer"):
        return "Please specify a layer for the map (e.g. the variable name in your data)."
    try:
        import canopy  # verify before cpu_bound (import happens in subprocess)
    except ImportError as e:
        return f"Canopy package not found or failed to load: {e}"
    return None


def _generate_figure(
    temp_dir: str,
    fields: list[str],
    figure: str,
    state: dict,
) -> tuple[tuple[str, str] | None, str | None]:
    """Run figure generation in a separate process. Returns ((output_path, json_path), error_msg)."""
    try:
        import canopy as cp
        json_path, output_path = build_and_save_config(fields, figure, state, temp_dir)
        cp.run_json(json_path)  # reads JSON, generates figure, writes image
        return (output_path, json_path), None
    except Exception as e:
        return None, str(e) if str(e) else "An error occurred. Check the terminal for the full traceback."


# -----------------------------------------------------------------------------
# Entry point guard (must be at end so create_content etc. are defined before main() blocks)
# -----------------------------------------------------------------------------

if __name__ in {"__main__", "__mp_main__"}:
    main()
