"""Docker Console tab: Containers."""

from __future__ import annotations

from typing import Any, List, Optional

import gi

gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1")
from gi.repository import Gtk, Adw, Pango  # noqa: E402

from .client import parse_published_ports  # noqa: E402
from .dialogs import (
    ContainerDetailsDialog, CreateContainerDialog, prompt_shell_options,
)  # noqa: E402
from . import widgets as w  # noqa: E402

from gettext import gettext as _  # noqa: E402


class ContainersTabMixin:
    def _build_containers_section(self) -> Gtk.Widget:
        box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)

        toolbar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
        self._container_search = Gtk.SearchEntry()
        self._container_search.set_placeholder_text(_("Filter by name / image / id…"))
        self._container_search.set_hexpand(True)
        self._container_search.connect("search-changed", self._on_container_search)
        toolbar.append(self._container_search)
        # Label + switch pair, same pattern as the Logs dialog timestamps switch.
        toolbar.append(Gtk.Label(label=_("Show stopped")))
        self._show_all_check = Gtk.Switch()
        self._show_all_check.set_active(
            bool(self.ctx.settings.get("show_all_containers", True)))
        self._show_all_check.set_valign(Gtk.Align.CENTER)
        self._show_all_check.connect("notify::active", self._on_show_all_toggled)
        toolbar.append(self._show_all_check)
        create = Gtk.Button()
        create.set_child(Adw.ButtonContent(icon_name="list-add-symbolic",
                                           label=_("New Container")))
        create.add_css_class("suggested-action")
        create.set_halign(Gtk.Align.END)
        create.connect("clicked", lambda _b: self._create_container())
        toolbar.append(create)
        box.append(toolbar)

        scroller = Gtk.ScrolledWindow()
        scroller.set_vexpand(True)
        self._containers_list = Gtk.ListBox()
        self._containers_list.add_css_class("boxed-list")
        self._containers_list.set_selection_mode(Gtk.SelectionMode.SINGLE)
        self._containers_list.connect("row-selected", self._on_container_row_selected)
        scroller.set_child(self._containers_list)
        self._containers_placeholder = self._make_loading_placeholder("Loading containers…")
        box.append(w.wrap_with_overlay(scroller, self._containers_placeholder))
        return box

    def _refresh_containers(self) -> None:
        client = self._client()
        if client is None or self._containers_busy:
            return
        self._containers_busy = True
        # Only show the loading logo on the first load after a host switch;
        # auto-refresh of an already-loaded view (even an empty or errored one)
        # updates silently (no flashing).
        if not self._containers_loaded:
            self._set_placeholder_loading(self._containers_placeholder, "Loading containers…")
        show_all = self._show_all_check.get_active()
        gen = self._load_gen
        self._run_async(lambda: client.ps(all=show_all),
                        lambda r, e, g=gen: self._on_containers(r, e, g))

    def _on_show_all_toggled(self, *_a) -> None:
        self.ctx.settings.set(
            "show_all_containers", bool(self._show_all_check.get_active()))
        self._refresh_containers()

    def _on_container_row_selected(self, _lb: Gtk.ListBox,
                                   row: Optional[Gtk.ListBoxRow]) -> None:
        if row is None:
            return
        box = row.get_child()
        cid = getattr(box, "_docker_cid", None)
        if not cid:
            return
        name = None
        for c in self._containers:
            if self._container_cid(c) == cid:
                name = w.field(c, "Names", "Name", default=cid[:12])
                break
        self._set_selected_container(cid, name or cid[:12], source="containers")

    def _on_containers(self, rows: Optional[List[dict]], err: Optional[Exception],
                       gen: int = 0) -> None:
        if gen != self._load_gen:
            return  # stale result for a previous host — drop it
        self._containers_busy = False
        self._containers_loaded = True
        if err is not None:
            w.clear_listbox(self._containers_list)
            self._containers = []
            self._set_placeholder_idle(self._containers_placeholder, w.error_text(err),
                                       error=True)
            self._refresh_logs_targets()
            return
        self._containers = rows or []
        self._sync_containers_list()
        self._refresh_logs_targets()
        self._restore_container_selection()

    def _restore_container_selection(self) -> None:
        """Re-select the shared selection after a list sync, if still present."""
        cid = getattr(self, "_selected_cid", None)
        if not cid:
            if self._containers:
                first = self._containers[0]
                self._set_selected_container(
                    self._container_cid(first),
                    w.field(first, "Names", "Name", default="container"),
                    source="auto")
            return
        child = self._containers_list.get_first_child()
        while child is not None:
            box = child.get_child()
            if getattr(box, "_docker_cid", None) == cid:
                self._containers_list.select_row(child)
                return
            child = child.get_next_sibling()
        # Selected container vanished — clear or pick first.
        if self._containers:
            first = self._containers[0]
            self._set_selected_container(
                self._container_cid(first),
                w.field(first, "Names", "Name", default="container"),
                source="auto")
        else:
            self._set_selected_container(None, None, source="auto")

    def _on_container_search(self, entry: Gtk.SearchEntry) -> None:
        self._container_query = entry.get_text().strip().lower()
        self._apply_container_filter()

    def _container_matches(self, c: dict) -> bool:
        if not self._container_query:
            return True
        hay = " ".join((
            w.field(c, "Names", "Name"), w.field(c, "Image"),
            w.field(c, "ID", "Id", "ContainerID"))).lower()
        return self._container_query in hay

    @staticmethod
    def _container_cid(c: dict) -> str:
        return w.field(c, "ID", "Id", "ContainerID")

    @staticmethod
    def _container_state_key(c: dict) -> str:
        state = w.field(c, "State", "Status").lower()
        if "paused" in state:
            state = "paused"
        elif "up" in state or "running" in state:
            state = "running"
        else:
            state = "stopped"
        # Ports participate so a gained/lost published port rebuilds the row
        # (the web-UI menu is only built in _fill_container_row).
        return f"{state}|{w.field(c, 'Ports')}"

    def _render_containers(self) -> None:
        """Full rebuild — used by tests; polling uses ``_sync_containers_list``."""
        w.clear_listbox(self._containers_list)
        if not self._containers:
            self._set_placeholder_idle(self._containers_placeholder, "No containers")
            return
        for c in self._containers:
            self._containers_list.append(self._container_row(c))
        self._apply_container_filter()

    def _sync_containers_list(self) -> None:
        """Merge polled container data into the list without a full clear."""
        if not self._containers:
            w.clear_listbox(self._containers_list)
            self._set_placeholder_idle(self._containers_placeholder, "No containers")
            return

        existing: dict[str, Gtk.ListBoxRow] = {}
        child = self._containers_list.get_first_child()
        while child is not None:
            box = child.get_child()
            cid = getattr(box, "_docker_cid", None)
            if cid:
                existing[cid] = child
            child = child.get_next_sibling()

        desired = [self._container_cid(c) for c in self._containers]
        desired_set = set(desired)
        for cid, lb_row in list(existing.items()):
            if cid not in desired_set:
                self._containers_list.remove(lb_row)
                existing.pop(cid, None)

        for index, c in enumerate(self._containers):
            cid = self._container_cid(c)
            lb_row = existing.get(cid)
            if lb_row is not None:
                box = lb_row.get_child()
                if not self._update_container_row_box(box, c):
                    new_row = self._container_row(c)
                    self._containers_list.remove(lb_row)
                    self._containers_list.insert(new_row, index)
                elif lb_row.get_index() != index:
                    # GTK4 ListBox has no reorder API — re-insert the same row.
                    self._containers_list.remove(lb_row)
                    self._containers_list.insert(lb_row, index)
            else:
                self._containers_list.insert(self._container_row(c), index)

        self._apply_container_filter()

    def _apply_container_filter(self) -> None:
        if not self._containers:
            return
        visible = 0
        child = self._containers_list.get_first_child()
        while child is not None:
            box = child.get_child()
            cid = getattr(box, "_docker_cid", None)
            data = next((c for c in self._containers if self._container_cid(c) == cid), None)
            show = data is not None and self._container_matches(data)
            child.set_visible(show)
            if show:
                visible += 1
            child = child.get_next_sibling()
        if visible == 0:
            self._set_placeholder_idle(
                self._containers_placeholder,
                "No matching containers" if self._container_query else "No containers",
            )
        else:
            self._hide_placeholder(self._containers_placeholder)

    @staticmethod
    def _health_of(status: str) -> Optional[str]:
        return w.health_of(status)

    @staticmethod
    def _set_status_dot(dot: Gtk.Image, *, running: bool, paused: bool) -> None:
        dot.set_from_icon_name(
            "media-record-symbolic" if running else "media-playback-stop-symbolic"
        )
        for cls in ("success", "warning", "dim-label"):
            dot.remove_css_class(cls)
        dot.add_css_class("success" if running else ("warning" if paused else "dim-label"))

    def _set_health_badge(self, row: Gtk.Box, status: str) -> None:
        health = w.health_of(status)
        badge = getattr(row, "_health_badge", None)
        if not health:
            if badge is not None:
                row.remove(badge)
                row._health_badge = None
            return
        hicon, hcls, htip = {
            "healthy": ("emblem-ok-symbolic", "success", "Healthy"),
            "unhealthy": ("dialog-warning-symbolic", "error", "Unhealthy"),
            "starting": ("content-loading-symbolic", "warning", "Health check starting"),
        }[health]
        if badge is None:
            badge = Gtk.Image.new_from_icon_name(hicon)
            row.insert_child_after(badge, row._status_dot)
            row._health_badge = badge
        else:
            badge.set_from_icon_name(hicon)
            for cls in ("success", "error", "warning"):
                badge.remove_css_class(cls)
        badge.add_css_class(hcls)
        badge.set_tooltip_text(htip)

    def _fill_container_row(self, row: Gtk.Box, c: dict) -> None:
        cid = self._container_cid(c)
        name = w.field(c, "Names", "Name", default=cid[:12])
        image = w.field(c, "Image")
        status = w.field(c, "Status", "State")
        ports = w.field(c, "Ports")
        state = w.field(c, "State", "Status").lower()
        running = "up" in state or "running" in state
        paused = "paused" in state

        row._docker_cid = cid
        row._state_key = self._container_state_key(c)

        dot = Gtk.Image()
        self._set_status_dot(dot, running=running, paused=paused)
        row.append(dot)
        row._status_dot = dot
        self._set_health_badge(row, status)

        info = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, hexpand=True)
        title = Gtk.Label(label=name, xalign=0)
        title.add_css_class("heading")
        info.append(title)
        sub = Gtk.Label(xalign=0)
        sub.add_css_class("dim-label")
        sub.add_css_class("caption")
        sub.set_text(" · ".join(p for p in (image, status, ports) if p))
        sub.set_ellipsize(Pango.EllipsizeMode.END)
        info.append(sub)
        row.append(info)
        row._title_lbl = title
        row._sub_lbl = sub

        published = parse_published_ports(ports)
        if published and (running or paused):
            self._add_web_menu(row, name, published)

        if running or paused:
            w.add_row_action(row, "media-playback-stop-symbolic", "Stop",
                             lambda: self._lifecycle("stop", cid, name))
            w.add_row_action(row, "view-refresh-symbolic", "Restart",
                             lambda: self._lifecycle("restart", cid, name))
            if paused:
                w.add_row_action(row, "media-playback-start-symbolic", "Resume",
                                 lambda: self._lifecycle("unpause", cid, name))
            else:
                w.add_row_action(row, "media-playback-pause-symbolic", "Pause",
                                 lambda: self._lifecycle("pause", cid, name))
            w.add_row_action(row, "process-stop-symbolic", "Kill",
                             lambda: self._lifecycle("kill", cid, name))
            w.add_row_action(row, "utilities-terminal-symbolic", "Open shell",
                             lambda: self._open_shell(cid, name), refreshes=False)
            w.add_row_action(row, "view-paged-symbolic", "Logs",
                             lambda: self._open_container_logs(cid, name, follow=True),
                             refreshes=False)
        else:
            w.add_row_action(row, "media-playback-start-symbolic", "Start",
                             lambda: self._lifecycle("start", cid, name))
            w.add_row_action(row, "view-paged-symbolic", "Logs",
                             lambda: self._open_container_logs(cid, name),
                             refreshes=False)
        w.add_row_action(row, "dialog-information-symbolic", "Details",
                         lambda: self._show_details(cid, name), refreshes=False)
        w.add_row_action(row, "preferences-system-symbolic", "Env",
                         lambda: self._show_details(cid, name, page="env"),
                         refreshes=False)
        w.add_row_action(row, "user-trash-symbolic", "Remove",
                         lambda: self._remove_container(cid, name))

    def _update_container_row_box(self, row: Gtk.Box, c: dict) -> bool:
        """Update labels/icons in place. Return False when action buttons must change."""
        if getattr(row, "_state_key", None) != self._container_state_key(c):
            return False
        cid = self._container_cid(c)
        name = w.field(c, "Names", "Name", default=cid[:12])
        image = w.field(c, "Image")
        status = w.field(c, "Status", "State")
        ports = w.field(c, "Ports")
        state = w.field(c, "State", "Status").lower()
        running = "up" in state or "running" in state
        paused = "paused" in state

        row._docker_cid = cid
        self._set_status_dot(row._status_dot, running=running, paused=paused)
        self._set_health_badge(row, status)
        row._title_lbl.set_label(name)
        row._sub_lbl.set_text(" · ".join(p for p in (image, status, ports) if p))
        return True

    def _container_row(self, c: dict) -> Gtk.Widget:
        row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
        row.set_margin_top(6)
        row.set_margin_bottom(6)
        row.set_margin_start(8)
        row.set_margin_end(8)
        self._fill_container_row(row, c)
        return w.listbox_wrap(row)

    def _show_details(self, cid: str, name: str, *, page: str = "overview") -> None:
        client = self._client()
        if client is None:
            return
        self._run_async(
            lambda: client.inspect(cid),
            lambda data, err: self._on_details(name, data, err, page=page),
        )

    def _on_details(self, name: str, data: Optional[dict], err: Optional[Exception],
                    *, page: str = "overview") -> None:
        if err is not None:
            self._toast(f"Inspect {name} failed: {err}")
            return
        if not data:
            self._toast(f"No details for {name}")
            return
        ContainerDetailsDialog(
            self._window(), name, data, initial_page=page).present()

    # --- container creation -------------------------------------------
    def _create_container(self) -> None:
        client = self._client()
        if client is None:
            self._toast("Select a host first")
            return
        images = [f"{w.field(i, 'Repository')}:{w.field(i, 'Tag')}"
                  for i in self._cached_images
                  if w.field(i, "Repository") not in ("", "<none>")]

        def on_create(spec: dict) -> None:
            self._run_async(
                lambda: client.create_container(spec.pop("image"), **spec),
                lambda res, err: self._on_action(
                    "create container", res, err, self._refresh_containers),
            )

        # Fetch the host's networks first so the dialog's Network picker is
        # populated; fall back to the built-in names if the probe fails.
        def open_dialog(rows: Optional[List[dict]], _err: Optional[Exception]) -> None:
            names = [w.field(n, "Name") for n in (rows or []) if w.field(n, "Name")]
            if not names:
                names = ["bridge", "host", "none"]
            CreateContainerDialog(self._window(), images, names, on_create).present()

        self._run_async(client.networks, open_dialog)

    def _lifecycle(self, action: str, cid: str, name: str) -> None:
        client = self._client()
        if client is None or cid in self._busy_cids:
            return  # an op is already in flight for this container — drop re-entrant
        self._busy_cids.add(cid)

        def done(res: Any, err: Optional[Exception]) -> None:
            self._busy_cids.discard(cid)
            self._on_action(f"{action} {name}", res, err, self._refresh_containers)

        self._run_async(lambda: client.lifecycle(action, cid), done)

    def _remove_container(self, cid: str, name: str) -> None:
        client = self._client()
        if client is None:
            return

        def do(force: bool) -> None:
            if cid in self._busy_cids:
                return
            self._busy_cids.add(cid)

            def done(res: Any, err: Optional[Exception]) -> None:
                self._busy_cids.discard(cid)
                self._on_action(f"remove {name}", res, err, self._refresh_containers)

            self._run_async(lambda: client.lifecycle("rm", cid, force=force), done)

        self._confirm(
            heading=_("Remove container?"),
            body=_("This will remove “{name}”.").format(name=name),
            destructive_label="Remove",
            on_confirm=do,
            force_label="Force (-f) — remove even if running",
        )

    # --- discovered web UIs (published TCP ports) ----------------------
    def _add_web_menu(self, row: Gtk.Box, name: str,
                      ports: List[tuple]) -> None:
        btn = Gtk.MenuButton()
        btn.set_icon_name("web-browser-symbolic")
        btn.set_tooltip_text(_("Open web UI"))
        btn.add_css_class("flat")
        btn.set_valign(Gtk.Align.CENTER)
        pop = Gtk.Popover()
        box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
        for host_port, container_port, scheme in ports:
            line = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
            lbl = Gtk.Label(label=f"{host_port} → {container_port}",
                            xalign=0, hexpand=True)
            line.append(lbl)
            for icon, tip, in_tab in (
                    ("tab-new-symbolic", "Open in tab", True),
                    ("adw-external-link-symbolic", "Open in browser", False)):
                b = Gtk.Button.new_from_icon_name(icon)
                b.add_css_class("flat")
                b.set_tooltip_text(tip)
                b.connect(
                    "clicked",
                    lambda _b, hp=host_port, s=scheme, t=in_tab: (
                        pop.popdown(), self._open_service(name, hp, s, t)))
                line.append(b)
            box.append(line)
        pop.set_child(box)
        btn.set_popover(pop)
        row.append(btn)

    def _open_service(self, name: str, host_port: int, scheme: str,
                      in_tab: bool) -> None:
        nick = self._current_nickname()
        if not nick:
            return
        if self._is_local(nick):  # local docker host — no tunnel needed
            self._present_service(name, f"{scheme}://localhost:{host_port}",
                                  in_tab)
            return

        def done(lp: Any, err: Optional[Exception]) -> None:
            if err is not None:
                self._toast(f"Port forward failed: {err}")
            else:
                self._present_service(name, f"{scheme}://localhost:{lp}",
                                      in_tab)

        self._run_async(
            lambda: self.ctx.ensure_local_forward(nick, host_port), done)

    def _present_service(self, name: str, url: str, in_tab: bool) -> None:
        if in_tab and callable(getattr(self.ctx.ui, "open_web_tab", None)):
            if self.ctx.ui.open_web_tab(url, title=name):
                return
        from ....web_tab import open_url_in_browser  # noqa: PLC0415
        if not open_url_in_browser(url):
            self._toast("Could not open browser")

    def _open_shell(self, cid: str, name: str) -> None:
        client = self._client()
        nick = self._current_nickname()
        if client is None or not nick:
            return

        def do_open(user: Optional[str], workdir: Optional[str]) -> None:
            ok = self._open_command_terminal(
                nick,
                client.exec_shell_command(cid, user=user, workdir=workdir),
                title=f"sh: {name}",
            )
            if not ok:
                self._toast("Could not open shell")

        prompt_shell_options(self._window(), name, do_open)

    def _follow_logs(self, cid: str, name: str) -> None:
        client = self._client()
        nick = self._current_nickname()
        if client is None or not nick:
            return
        cmd = client.logs_follow_command(
            cid, tail=int(self._tail_spin.get_value()),
            timestamps=self._ts_switch.get_active(),
        )
        ok = self._open_command_terminal(nick, cmd, title=f"logs: {name}")
        if not ok:
            self._toast("Could not open logs")

    # ================================================================
