from __future__ import annotations

import html
import subprocess
import warnings
from typing import Callable

from device_auth import DeviceAuthPrompt

WARNING_COPY = (
    "If this ChatGPT account is already logged in anywhere else (this tray, another machine, "
    "a browser session), completing this will immediately sign that session out. Only continue "
    "if you're adding a brand-new account or intentionally re-authenticating one you control."
)


def _build_existing_accounts_lines(
    existing_accounts: list[tuple[str, str]] | None,
) -> list[str]:
    if not existing_accounts:
        return []
    return [f"{alias} — {email}" for alias, email in existing_accounts]


class _FallbackClipboard:
    def __init__(self) -> None:
        self.text = ""

    def set_text(self, text: str, _length: int) -> None:
        self.text = text


class _FallbackWidget:
    def __init__(self, label: str = "") -> None:
        self.label = label
        self.visible = True
        self.sensitive = True
        self.children: list[object] = []

    def show(self) -> None:
        self.visible = True

    def hide(self) -> None:
        self.visible = False

    def show_all(self) -> None:
        self.visible = True
        for child in self.children:
            if hasattr(child, "show_all"):
                child.show_all()
            elif hasattr(child, "show"):
                child.show()

    def set_sensitive(self, sensitive: bool) -> None:
        self.sensitive = sensitive

    def set_markup(self, markup: str) -> None:
        self.label = markup

    def set_text(self, text: str) -> None:
        self.label = text


class _FallbackLabel(_FallbackWidget):
    def __init__(self, label: str = "") -> None:
        super().__init__(label=label)
        self.selectable = False
        self.xalign = 0.0
        self.line_wrap = False

    def set_selectable(self, selectable: bool) -> None:
        self.selectable = selectable

    def set_xalign(self, xalign: float) -> None:
        self.xalign = xalign

    def set_line_wrap(self, line_wrap: bool) -> None:
        self.line_wrap = line_wrap


class _FallbackEntry(_FallbackWidget):
    def __init__(self, text: str = "") -> None:
        super().__init__()
        self.text = text

    def set_text(self, text: str) -> None:
        self.text = text

    def get_text(self) -> str:
        return self.text


class _FallbackButton(_FallbackWidget):
    def __init__(self, label: str = "") -> None:
        super().__init__(label=label)
        self._clicked = None

    def connect(self, signal: str, callback) -> None:
        if signal == "clicked":
            self._clicked = callback

    def clicked(self) -> None:
        if self._clicked is not None:
            self._clicked(self)

    def click(self) -> None:
        self.clicked()


class _FallbackLinkButton(_FallbackButton):
    def __init__(self, uri: str, label: str) -> None:
        super().__init__(label=label)
        self.uri = uri


class _FallbackBox(_FallbackWidget):
    def append_child(self, child: object) -> None:
        self.children.append(child)

    def pack_start(self, child: object, *_args) -> None:
        self.append_child(child)

    def add(self, child: object) -> None:
        self.append_child(child)


class _FallbackContentArea(_FallbackBox):
    pass


class _FallbackDialog:
    def __init__(self, title: str | None = None) -> None:
        self.title = title
        self.modal = True
        self.default_size = (0, 0)
        self.destroyed = False
        self.content_area = _FallbackContentArea()
        self._signals: dict[str, object] = {}

    def set_modal(self, modal: bool) -> None:
        self.modal = modal

    def set_default_size(self, width: int, height: int) -> None:
        self.default_size = (width, height)

    def get_content_area(self) -> _FallbackContentArea:
        return self.content_area

    def show_all(self) -> None:
        self.content_area.show_all()

    def connect(self, signal: str, callback) -> None:
        self._signals[signal] = callback

    def emit(self, signal: str, *args):
        callback = self._signals.get(signal)
        if callback is None:
            return None
        return callback(self, *args)

    def destroy(self) -> None:
        if self.destroyed:
            return
        self.destroyed = True
        callback = self._signals.get("destroy")
        if callback is not None:
            callback(self)


class _FallbackClipboardModule:
    _clipboard = _FallbackClipboard()

    @classmethod
    def get(cls, *_args) -> _FallbackClipboard:
        return cls._clipboard


class _FallbackGdk:
    SELECTION_CLIPBOARD = "clipboard"


class _FallbackGtk:
    Dialog = _FallbackDialog
    Box = _FallbackBox
    Label = _FallbackLabel
    Entry = _FallbackEntry
    Button = _FallbackButton
    LinkButton = _FallbackLinkButton
    Clipboard = _FallbackClipboardModule

    class Orientation:
        VERTICAL = 1
        HORIZONTAL = 2

    @staticmethod
    def Box_new(*_args, **_kwargs) -> _FallbackBox:
        return _FallbackBox()


def _load_gtk():
    try:
        import gi

        warnings.filterwarnings(
            "ignore",
            message="GLib.unix_signal_add_full is deprecated; use GLibUnix.signal_add_full instead",
            category=DeprecationWarning,
        )
        gi.require_version("Gtk", "3.0")
        gi.require_version("Gdk", "3.0")
        from gi.repository import Gdk, Gtk

        initialized, _argv = Gtk.init_check(None)
        if initialized:
            return Gtk, Gdk, True
    except Exception:
        pass
    return _FallbackGtk, _FallbackGdk, False


Gtk, Gdk, GTK_AVAILABLE = _load_gtk()


class ClaudeCodeEntryDialog(Gtk.Dialog):
    @property
    def modal(self) -> bool:
        if hasattr(self, "_modal"):
            return self._modal
        try:
            return bool(super().get_modal())
        except Exception:
            return False

    @modal.setter
    def modal(self, modal: bool) -> None:
        self._modal = bool(modal)
        try:
            super().set_modal(modal)
        except Exception:
            pass

    def __init__(
        self,
        url: str,
        on_submit: Callable[[str], None],
        on_cancel: Callable[[], None],
    ) -> None:
        super().__init__(title="Claude login code entry")
        self.url = url
        self.on_submit = on_submit
        self.on_cancel = on_cancel
        self._cancelled = False
        self._submitted = False
        self.link_button = DeviceAuthDialog._new_link_button(url, url)
        self.code_entry = Gtk.Entry()
        self.submit_button = DeviceAuthDialog._new_button("Submit")
        self.cancel_button = DeviceAuthDialog._new_button("Cancel")
        self.failure_label = DeviceAuthDialog._new_label("")

        self.modal = False
        self._configure_widgets()
        self._build_layout()

    def _configure_widgets(self) -> None:
        self.submit_button.connect("clicked", self._handle_submit)
        self.cancel_button.connect("clicked", self._handle_cancel)
        if hasattr(self, "connect"):
            self.connect("delete-event", self._handle_cancel)
        self.failure_label.hide()

    def _build_layout(self) -> None:
        container = DeviceAuthDialog._new_box()
        content = self.get_content_area()
        DeviceAuthDialog._pack(content, container)
        DeviceAuthDialog._pack(container, self.link_button)
        DeviceAuthDialog._pack(container, self.code_entry)
        DeviceAuthDialog._pack(container, self.submit_button)
        DeviceAuthDialog._pack(container, self.cancel_button)
        DeviceAuthDialog._pack(container, self.failure_label)
        self.show_all()
        if hasattr(self, "present"):
            self.present()
        self.failure_label.hide()

    def _handle_submit(self, _widget) -> None:
        if self._submitted or self._cancelled:
            return
        self._submitted = True
        self.code_entry.set_sensitive(False)
        self.submit_button.set_sensitive(False)
        self.on_submit(self.code_entry.get_text())

    def _handle_cancel(self, *_args) -> bool:
        if self._cancelled:
            return True
        self._cancelled = True
        try:
            self.on_cancel()
        finally:
            self.destroy()
        return True

    def close_success(self) -> None:
        self.destroy()

    def show_failure(self, retry_available: bool) -> None:
        del retry_available
        self.code_entry.set_sensitive(False)
        self.submit_button.set_sensitive(False)
        self.cancel_button.set_sensitive(False)
        self.failure_label.set_text("Claude login failed. Close this dialog and try again.")
        self.failure_label.show()


class DeviceAuthDialog(Gtk.Dialog):
    @property
    def modal(self) -> bool:
        if hasattr(self, "_modal"):
            return self._modal
        try:
            return bool(super().get_modal())
        except Exception:
            return False

    @modal.setter
    def modal(self, modal: bool) -> None:
        self._modal = bool(modal)
        try:
            super().set_modal(modal)
        except Exception:
            pass

    def __init__(
        self,
        prompt: DeviceAuthPrompt,
        on_retry: Callable[[], None] | None = None,
        on_cancel: Callable[[], None] | None = None,
        existing_accounts: list[tuple[str, str]] | None = None,
    ) -> None:
        super().__init__(title="Codex device authorization")
        self.prompt = prompt
        self.on_retry = on_retry
        self.on_cancel = on_cancel
        self.existing_accounts = existing_accounts or []
        self._cancelled = False
        self.firefox_error_label = self._new_label("")
        self.failure_label = self._new_label("")
        self.retry_button = self._new_button("Retry")
        self.cancel_button = self._new_button("Cancel")
        self.copy_code_button = self._new_button("Copy code")
        self.copy_text_button = self._new_button("Copy full text")
        self.open_button = self._new_button("Open in Firefox")
        self.link_button = self._new_link_button(prompt.url, prompt.url)
        self.code_label = self._new_label("")
        self.warning_label = self._new_label(WARNING_COPY)
        self.existing_accounts_label = self._new_label(
            "\n".join(_build_existing_accounts_lines(self.existing_accounts))
        )
        self.status_label = self._new_label("This device code expires in 15 minutes.")

        self.modal = False
        if hasattr(self, "set_default_size"):
            self.set_default_size(560, 360)

        self._configure_widgets()
        self._build_layout()

    def show_failure(self, retry_available: bool) -> None:
        self.failure_label.set_text("Authentication did not complete. Retry from this dialog.")
        self.failure_label.show()
        self.retry_button.set_sensitive(retry_available)
        if retry_available:
            self.retry_button.show()
        else:
            self.retry_button.hide()

    def close_success(self) -> None:
        self.destroy()

    def _configure_widgets(self) -> None:
        self.code_label.set_markup(
            f"<span font_desc='monospace 18'><b>{html.escape(self.prompt.code)}</b></span>"
        )
        self.firefox_error_label.hide()
        self.failure_label.hide()
        self.warning_label.set_line_wrap(True)
        self.warning_label.set_xalign(0.0)
        self.warning_label.set_selectable(True)
        self.existing_accounts_label.set_xalign(0.0)
        self.existing_accounts_label.set_selectable(True)
        self.existing_accounts_label.set_line_wrap(True)
        self.code_label.set_selectable(True)
        self.code_label.set_xalign(0.0)
        self.status_label.set_xalign(0.0)
        self.status_label.set_line_wrap(True)
        self.firefox_error_label.set_xalign(0.0)
        self.firefox_error_label.set_line_wrap(True)
        self.failure_label.set_xalign(0.0)
        self.failure_label.set_line_wrap(True)

        self.open_button.connect("clicked", self._handle_open_clicked)
        self.link_button.connect("clicked", self._handle_open_clicked)
        self.copy_code_button.connect("clicked", self._handle_copy_code)
        self.copy_text_button.connect("clicked", self._handle_copy_full_text)
        self.retry_button.connect("clicked", self._handle_retry)
        self.cancel_button.connect("clicked", self._handle_cancel)
        if hasattr(self, "connect"):
            self.connect("delete-event", self._handle_cancel)
        if self.on_retry is None:
            self.retry_button.hide()

    def _build_layout(self) -> None:
        container = self._new_box()
        content = self.get_content_area()
        self._pack(content, container)

        if self.existing_accounts:
            self._pack(container, self.warning_label)
            self._pack(container, self.existing_accounts_label)

        self._pack(container, self.link_button)
        self._pack(container, self.open_button)
        self._pack(container, self.code_label)
        self._pack(container, self.copy_code_button)
        self._pack(container, self.copy_text_button)
        self._pack(container, self.status_label)
        self._pack(container, self.firefox_error_label)
        self._pack(container, self.failure_label)
        self._pack(container, self.retry_button)
        self._pack(container, self.cancel_button)
        self.show_all()
        if hasattr(self, "present"):
            self.present()
        self.firefox_error_label.hide()
        self.failure_label.hide()
        if self.on_retry is None:
            self.retry_button.hide()

    def _handle_open_clicked(self, _widget) -> None:
        try:
            subprocess.Popen(["firefox", self.prompt.url])
            self.firefox_error_label.hide()
        except Exception as exc:
            self.firefox_error_label.set_text(f"Could not open Firefox automatically: {exc}")
            self.firefox_error_label.show()

    def _handle_copy_code(self, _widget) -> None:
        self._clipboard().set_text(self.prompt.code, -1)

    def _handle_copy_full_text(self, _widget) -> None:
        self._clipboard().set_text(self.prompt.raw_text, -1)

    def _handle_retry(self, _widget) -> None:
        if self.on_retry is not None:
            self.on_retry()

    def _handle_cancel(self, *_args) -> bool:
        if self._cancelled:
            return True
        self._cancelled = True
        try:
            if self.on_cancel is not None:
                self.on_cancel()
        finally:
            self.destroy()
        return True

    @staticmethod
    def _new_box():
        try:
            return Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
        except TypeError:
            return Gtk.Box()

    @staticmethod
    def _new_label(text: str):
        try:
            return Gtk.Label(label=text)
        except TypeError:
            return Gtk.Label(text)

    @staticmethod
    def _new_button(label: str):
        try:
            return Gtk.Button(label=label)
        except TypeError:
            return Gtk.Button(label)

    @staticmethod
    def _new_link_button(uri: str, label: str):
        try:
            return Gtk.LinkButton.new_with_label(uri, label)
        except AttributeError:
            return Gtk.LinkButton(uri, label)

    @staticmethod
    def _clipboard():
        return Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)

    @staticmethod
    def _pack(parent, child) -> None:
        if hasattr(parent, "pack_start"):
            parent.pack_start(child, False, False, 0)
        elif hasattr(parent, "add"):
            parent.add(child)
        elif hasattr(parent, "append_child"):
            parent.append_child(child)
