import json
import os

import gi
gi.require_version("Gdk", "4.0")
from gi.repository import Gio, GLib, Gdk

import stillControl.constants as constants
import stillControl.Utils as Utils
from stillControl.i18n import _

_LAYOUTS_UI = os.path.join(os.path.dirname(__file__), "layouts")
_monitor_specific_panel_settings = [
    "panel-anchors", "panel-element-positions",
    "panel-lengths", "panel-positions", "panel-sizes"
]


def get_minimum_monitor_height():
    """
    Get the minimum height across all connected monitors.
    This is useful for ensuring UI elements like menus don't exceed screen bounds.
    Returns the minimum height, or None if no monitors are found.
    """
    display = Gdk.Display.get_default()
    if not display:
        return None
    
    monitors = display.get_monitors()
    if not monitors or monitors.get_n_items() == 0:
        return None
    
    min_height = None
    for i in range(monitors.get_n_items()):
        monitor = monitors.get_item(i)
        geometry = monitor.get_geometry()
        if min_height is None or geometry.height < min_height:
            min_height = geometry.height
    
    return min_height


def import_settings(schema):
    """
    Import a GSettings schema safely. Returns None if the schema doesn't exist.
    This allows the application to work even if some extensions aren't installed.
    """
    try:
        setting = Gio.Settings.new(schema)
        setting.delay()
        return setting
    except Exception as e:
        print(f"Info: Schema '{schema}' not available (extension may not be installed)")
        return None


_shell_settings = import_settings("org.gnome.shell")
_panel_settings = import_settings("org.gnome.shell.extensions.dash-to-panel")

_extension_settings = {  # Panel settings not included, needs to be treated separate due to monitor specific settings
    "dash-to-dock@micxgx.gmail.com": import_settings("org.gnome.shell.extensions.dash-to-dock"),
    "arcmenu@arcmenu.com": import_settings("org.gnome.shell.extensions.arcmenu"),
    "just-perfection-desktop@just-perfection": import_settings("org.gnome.shell.extensions.just-perfection"),
    "gtk4-ding@smedius.gitlab.com": import_settings("org.gnome.shell.extensions.gtk4-ding")
}

# Flag to prevent callbacks from modifying settings during layout switching
_layout_switching = False


def get_settings_list():
    settings = []
    if _panel_settings:
        settings.append(_panel_settings)
    for ext in _extension_settings:
        if _extension_settings[ext]:
            settings.append(_extension_settings[ext])
    return settings


def apply_settings():
    settings = get_settings_list()
    settings.append(_shell_settings)
    for setting in settings:
        setting.apply()


def set_extensions(to_enable, to_disable):
    enabled_extensions = _shell_settings.get_strv("enabled-extensions")
    disabled_extensions = _shell_settings.get_strv("disabled-extensions")
    for extension in to_disable:
        while extension in enabled_extensions:
            enabled_extensions.remove(extension)
        if extension not in disabled_extensions:
            disabled_extensions.append(extension)
    for extension in to_enable:
        if extension not in enabled_extensions:
            enabled_extensions.append(extension)
        while extension in disabled_extensions:
            disabled_extensions.remove(extension)

    _shell_settings.set_strv("enabled-extensions", enabled_extensions)
    _shell_settings.set_strv("disabled-extensions", disabled_extensions)


def check_extensions(enabled, disabled):
    if not _shell_settings:
        return False
    enabled_extensions = _shell_settings.get_strv("enabled-extensions")
    disabled_extensions = _shell_settings.get_strv("disabled-extensions")
    for extension in enabled:
        if extension not in enabled_extensions or extension in disabled_extensions:
            return False
    for extension in disabled:
        if extension in enabled_extensions and extension not in disabled_extensions:
            return False
    return True


def reset_settings():
    for setting in get_settings_list():
        settings_schema = setting.props.settings_schema
        keys_to_reset = settings_schema.list_keys()

        # check if there's exclusion rules
        if settings_schema.get_id() in constants.SETTINGS_FOR_RESET_EXCLUDE.keys():
            for key in constants.SETTINGS_FOR_RESET_EXCLUDE[settings_schema.get_id()]:
                while key in keys_to_reset:
                    keys_to_reset.remove(key)
        for key in keys_to_reset:
            setting.reset(key)


def set_panel_settings(settings):
    if not _panel_settings:
        print("Warning: Panel settings schema not available, skipping panel configuration")
        return
    
    for key in settings:
        if key in _monitor_specific_panel_settings:
            try:
                set_monitor_specific_panel_setting(key, settings[key])
            except Exception as e:
                print(f"Warning: Failed to set panel setting '{key}': {e}")
                print(f"  This property may not be available in the current extension version.")
        else:
            Utils.safe_set_setting(_panel_settings, key, settings[key], "panel setting")


def check_panel_settings(settings):
    if not _panel_settings:
        return False
    
    for key in settings:
        if key in _monitor_specific_panel_settings:
            if not check_monitor_specific_panel_setting(key, settings[key]):
                return False
        else:
            try:
                if Utils.serialize_setting(_panel_settings, key) != settings[key]:
                    return False
            except Exception:
                # If we can't read a setting, it doesn't match
                return False
    return True


def set_extension_settings(extension_uuid, layout_settings):
    if extension_uuid not in _extension_settings:
        print(f"Warning: Extension '{extension_uuid}' settings not available, skipping")
        return
    
    if _extension_settings[extension_uuid]:
        # For Arc Menu, ensure icon-related settings are applied in the correct order
        # to prevent the icon from disappearing until shell restart
        if extension_uuid == "arcmenu@arcmenu.com":
            # If menu-height exceeds screen resolution, use the default value instead
            if "menu-height" in layout_settings:
                min_height = get_minimum_monitor_height()
                if min_height is not None and layout_settings["menu-height"] > min_height:
                    print(f"Info: Layout menu-height ({layout_settings['menu-height']}) exceeds screen height ({min_height}), using default")
                    del layout_settings["menu-height"]  # Remove so it stays at/resets to default
            
            # Apply custom icon path FIRST, then the icon mode.
            priority_order = ["menu-button-icon"]
            for key in priority_order:
                if key in layout_settings:
                    Utils.safe_set_setting(_extension_settings[extension_uuid], key, layout_settings[key], f"{extension_uuid} setting")
            
            # Then apply remaining settings
            for key in layout_settings:
                if key not in priority_order:
                    Utils.safe_set_setting(_extension_settings[extension_uuid], key, layout_settings[key], f"{extension_uuid} setting")
        else:
            for key in layout_settings:
                Utils.safe_set_setting(_extension_settings[extension_uuid], key, layout_settings[key], f"{extension_uuid} setting")


def check_extension_settings(extension_uuid, layout_settings):
    if extension_uuid not in _extension_settings or not _extension_settings[extension_uuid]:
        return True
    settings = _extension_settings[extension_uuid]
    available_keys = set(settings.props.settings_schema.list_keys())
    for key in layout_settings:
        # Layouts are shared across extension versions. Ignore settings that
        # the installed version does not expose instead of asking Gio for an
        # invalid key (which terminates the process).
        if key not in available_keys:
            continue
        try:
            if Utils.serialize_setting(settings, key) != layout_settings[key]:
                return False
        except Exception:
            # If we can't read a setting, it doesn't match
            return False
    return True


def split_setting(setting):
    split = setting.split(".")
    key = split.pop()
    schema = ".".join(split)

    return schema, key


def set_gsetting(setting, value):
    schema, key = split_setting(setting)
    try:
        settings = Gio.Settings.new(schema)
        settings.set_value(key, value)
    except Exception as e:
        print(f"Warning: Failed to set gsetting '{setting}': {e}")
        print(f"  This schema or key may not be available in the current system.")


def set_gsettings(json):
    for key in json:
        set_gsetting(key, json[key])


def check_setting(setting, value):
    schema, key = split_setting(setting)
    settings = Gio.Settings.new(schema)
    return Utils.serialize_setting(settings, key) == value


def check_gsettings(json):
    for key in json:
        if not check_setting(key, json[key]):
            return False
    return True


def get_all_monitor_ids():
    """
    Get the identifiers Dash to Panel uses for connected monitors.

    Dash to Panel uses the same stable IDs Mutter reports (normally
    ``vendor-serial``) as keys in per-monitor JSON settings.
    """
    if not _panel_settings:
        return ["0"]

    # This mirrors Dash to Panel's setMonitorsInfo() ID generation.
    try:
        bus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
        state = bus.call_sync(
            "org.gnome.Mutter.DisplayConfig",
            "/org/gnome/Mutter/DisplayConfig",
            "org.gnome.Mutter.DisplayConfig",
            "GetCurrentState",
            None,
            None,
            Gio.DBusCallFlags.NONE,
            2000,
            None,
        ).unpack()
        monitor_ids = []
        for index, logical_monitor in enumerate(state[2]):
            connector, vendor, _product, serial = logical_monitor[5][0]
            monitor_id = f"{vendor}-{serial}" if vendor and serial else str(index)
            if monitor_id in monitor_ids:
                monitor_id = connector if connector and connector not in monitor_ids else str(index)
            monitor_ids.append(str(monitor_id))
        if monitor_ids:
            return monitor_ids
    except Exception as error:
        print(f"Info: Could not query Mutter monitor IDs: {error}")

    # The extension may already have populated its per-monitor settings. This
    # also makes the code usable outside a live GNOME session.
    monitor_ids = []
    for key in _monitor_specific_panel_settings:
        try:
            values = json.loads(_panel_settings.get_string(key))
            for monitor_id in values:
                if monitor_id not in monitor_ids:
                    monitor_ids.append(monitor_id)
        except Exception:
            pass
    if monitor_ids:
        return monitor_ids

    return ["0"]


def check_monitor_specific_panel_setting(key, value):
    if not _panel_settings:
        return False
    try:
        settings_str = _panel_settings.get_string(key)
        if not settings_str:
            return False
        settings = json.loads(settings_str)
        if not settings:
            return False
        for monitor in settings:
            if settings[monitor] != value:
                return False
        return True
    except (json.JSONDecodeError, Exception):
        return False


def set_monitor_specific_panel_setting(key, value):
    """
    Set a panel setting that should be applied uniformly across all monitors.
    This ensures the same layout is applied to every connected monitor.
    """
    if not _panel_settings:
        print(f"Warning: Cannot set panel setting '{key}' - panel settings not available")
        return
    
    # Get the stable IDs of all connected monitors.
    monitors = get_all_monitor_ids()
    
    # Preserve existing monitor keys as well, including disconnected displays.
    try:
        current_str = _panel_settings.get_string(key)
        if current_str:
            current_settings = json.loads(current_str)
            if current_settings:
                for monitor_key in current_settings.keys():
                    if monitor_key not in monitors:
                        monitors.append(monitor_key)
    except (json.JSONDecodeError, Exception):
        pass
    
    # Build the new settings dict with the value applied to ALL monitors
    new_settings = {}
    for monitor in monitors:
        new_settings[str(monitor)] = value
    
    print(f"Setting {key} for monitors {list(new_settings.keys())} to value")
    _panel_settings.set_string(key, json.dumps(new_settings))


def _get_direct_panel_settings():
    try:
        return Gio.Settings.new("org.gnome.shell.extensions.dash-to-panel")
    except Exception:
        return None


def _load_panel_element_settings(panel_settings):
    current_str = panel_settings.get_string("panel-element-positions")
    if not current_str:
        return {}

    try:
        current_settings = json.loads(current_str)
        return current_settings if isinstance(current_settings, dict) else {}
    except json.JSONDecodeError:
        return {}


def _get_panel_element_template(current_settings):
    for monitor in current_settings:
        if isinstance(current_settings[monitor], list) and current_settings[monitor]:
            return current_settings[monitor]
    return None


def _ensure_panel_element(positions, element_name, fallback_position="stackedTL"):
    for element in positions:
        if isinstance(element, dict) and element.get("element") == element_name:
            return element

    element = {
        "element": element_name,
        "visible": False,
        "position": fallback_position,
    }
    positions.append(element)
    return element


def _move_panel_element(positions, element_name, target_index):
    element_index = None
    for index, element in enumerate(positions):
        if isinstance(element, dict) and element.get("element") == element_name:
            element_index = index
            break

    if element_index is None:
        return

    element = positions.pop(element_index)
    target_index = max(0, min(target_index, len(positions)))
    positions.insert(target_index, element)


def _move_panel_element_after(positions, element_name, previous_element_name):
    element_index = None
    previous_index = None
    for index, element in enumerate(positions):
        if not isinstance(element, dict):
            continue
        if element.get("element") == element_name:
            element_index = index
        elif element.get("element") == previous_element_name:
            previous_index = index

    if element_index is None or previous_index is None:
        return

    element = positions.pop(element_index)
    if element_index < previous_index:
        previous_index -= 1
    positions.insert(previous_index + 1, element)


def set_panel_element_visibility(element_name: str, visible: bool, fallback_position="stackedTL"):
    """
    Set a Dash to Panel element visibility across all monitors.
    
    Note: This uses a direct (non-delayed) settings connection to avoid interfering
    with layout switching operations.
    """
    panel_settings = _get_direct_panel_settings()
    if panel_settings is None:
        print(f"Warning: Cannot set {element_name} visibility - panel settings not available")
        return
    
    try:
        current_settings = _load_panel_element_settings(panel_settings)
        
        # If we have no settings at all, we can't modify them - Dash to Panel needs to initialize first
        if not current_settings:
            print("Warning: No panel-element-positions settings found - Dash to Panel may not be configured")
            return
        
        monitors = list(current_settings.keys())
        
        # Get the first monitor's element positions as template if available
        template_positions = _get_panel_element_template(current_settings)
        
        if template_positions is None:
            print("Warning: No element positions template found")
            return
        
        # Update element visibility in all monitors
        new_settings = {}
        for monitor in monitors:
            if monitor in current_settings and current_settings[monitor]:
                # Deep copy the positions list to avoid modifying the original
                positions = json.loads(json.dumps(current_settings[monitor]))
            else:
                positions = json.loads(json.dumps(template_positions))
            
            element = _ensure_panel_element(positions, element_name, fallback_position)
            element["visible"] = visible
            
            new_settings[str(monitor)] = positions
        
        # Set directly (this will trigger immediate change notification)
        panel_settings.set_string("panel-element-positions", json.dumps(new_settings))
        print(f"Set {element_name} visibility to {visible} on monitors {list(new_settings.keys())}")
    except Exception as e:
        print(f"Warning: Failed to set {element_name} visibility: {e}")


def set_activities_button_visibility(visible: bool):
    """
    Set the activities button visibility across all monitors in Dash to Panel.
    This is used to hide the stock activities button when Arc Menu is enabled.
    """
    set_panel_element_visibility("activitiesButton", visible)


def set_show_apps_button_visibility(visible: bool):
    """
    Set the Show Applications button visibility across all monitors in Dash to Panel.
    This is used to hide Dash to Panel's app launcher when Arc Menu is enabled.
    """
    set_panel_element_visibility("showAppsButton", visible)


def set_arc_menu_panel_elements_enabled(enabled: bool, show_activities: bool = False):
    """
    Keep Dash to Panel's stock launchers and leftBox order in sync with Arc Menu.

    Arc Menu lives in leftBox. When enabled, leftBox needs to be the first left-side
    item and Dash to Panel's Show Applications button should be hidden. When Arc
    Menu is disabled, restore Activities and put leftBox back behind it.
    """
    panel_settings = _get_direct_panel_settings()
    if panel_settings is None:
        print("Warning: Cannot sync Arc Menu panel elements - panel settings not available")
        return

    try:
        current_settings = _load_panel_element_settings(panel_settings)
        if not current_settings:
            print("Warning: No panel-element-positions settings found - Dash to Panel may not be configured")
            return

        template_positions = _get_panel_element_template(current_settings)
        if template_positions is None:
            print("Warning: No element positions template found")
            return

        new_settings = {}
        for monitor, monitor_positions in current_settings.items():
            if isinstance(monitor_positions, list) and monitor_positions:
                positions = json.loads(json.dumps(monitor_positions))
            else:
                positions = json.loads(json.dumps(template_positions))

            show_apps = _ensure_panel_element(positions, "showAppsButton")
            activities = _ensure_panel_element(positions, "activitiesButton")
            left_box = _ensure_panel_element(positions, "leftBox")
            activities["position"] = "stackedTL"
            left_box["position"] = "stackedTL"

            if enabled:
                show_apps["visible"] = False
                activities["visible"] = bool(show_activities)
                _move_panel_element(positions, "leftBox", 0)
            else:
                show_apps["visible"] = True
                activities["visible"] = True
                _move_panel_element_after(positions, "leftBox", "activitiesButton")

            new_settings[str(monitor)] = positions

        panel_settings.set_string("panel-element-positions", json.dumps(new_settings))
        print(f"Synced Arc Menu panel elements on monitors {list(new_settings.keys())}")
    except Exception as e:
        print(f"Warning: Failed to sync Arc Menu panel elements: {e}")


def is_dash_to_panel_enabled():
    """Check if Dash to Panel extension is currently enabled."""
    if not _shell_settings:
        return False
    enabled_extensions = _shell_settings.get_strv("enabled-extensions")
    disabled_extensions = _shell_settings.get_strv("disabled-extensions")
    uuid = "dash-to-panel@jderose9.github.com"
    return uuid in enabled_extensions and uuid not in disabled_extensions


def is_layout_switching():
    """Check if a layout switch is currently in progress."""
    return _layout_switching


def set_layout_from_dict(layout: dict):
    global _layout_switching
    _layout_switching = True
    
    try:
        reset_settings()
        
        # Figure out extensions to enable and disable
        if "dash-to-panel@jderose9.github.com" in layout:
            set_panel_settings(layout["dash-to-panel@jderose9.github.com"])

        for ext in list(_extension_settings.keys()):
            if ext in layout:
                set_extension_settings(ext, layout[ext])

        set_extensions(layout["enabled_extensions"], layout["disabled_extensions"])

        if "gsettings" in layout:
            set_gsettings(layout["gsettings"])

        apply_settings()
    finally:
        _layout_switching = False


def set_layout(layout: str):
    json_file = f"{_LAYOUTS_UI}/{layout}.json"

    if os.path.exists(json_file):
        with open(json_file, "r") as file:
            set_layout_from_dict(layout_as_dict(file.read()))
    else:
        raise FileNotFoundError(f"Layout file {json_file} not found in {_LAYOUTS_UI} directory")


def layout_as_dict(json_str: str):
    layout = json.loads(json_str)

    for d in ["enabled_extensions", "disabled_extensions"]:
        if d not in layout:
            layout[d] = []

    extensions = ["dash-to-panel@jderose9.github.com"] + list(_extension_settings.keys())
    for filename in sorted(os.listdir(_LAYOUTS_UI)):
        if not filename.endswith(".json"):
            continue
        try:
            with open(os.path.join(_LAYOUTS_UI, filename), "r") as file:
                candidate = json.load(file)
        except (OSError, json.JSONDecodeError):
            continue
        for key in candidate:
            if "@" in key and key not in extensions:
                extensions.append(key)
        for list_name in ["enabled_extensions", "disabled_extensions"]:
            explicit_extensions = candidate.get(list_name, [])
            if not isinstance(explicit_extensions, list):
                continue
            for extension in explicit_extensions:
                if isinstance(extension, str) and extension not in extensions:
                    extensions.append(extension)

    for key in layout:
        if "@" in key and key not in extensions:
            extensions.append(key)

    enabled_extensions = list(dict.fromkeys(layout["enabled_extensions"]))
    disabled_extensions = list(dict.fromkeys(layout["disabled_extensions"]))

    for extension in enabled_extensions + disabled_extensions:
        if extension not in extensions:
            extensions.append(extension)

    for extension in extensions:
        if extension in layout or extension in enabled_extensions:
            if extension not in enabled_extensions:
                enabled_extensions.append(extension)
            while extension in disabled_extensions:
                disabled_extensions.remove(extension)
        elif extension not in disabled_extensions:
            disabled_extensions.append(extension)

    layout["enabled_extensions"] = enabled_extensions
    layout["disabled_extensions"] = disabled_extensions

    return layout


def check_layout_dict(layout: dict):
    if not check_extensions(layout["enabled_extensions"], layout["disabled_extensions"]):
        return False

    if "dash-to-panel@jderose9.github.com" in layout:
        if not check_panel_settings(layout["dash-to-panel@jderose9.github.com"]):
            return False

    for ext in _extension_settings:
        if ext in layout:
            if not check_extension_settings(ext, layout[ext]):
                return False

    if "gsettings" in layout:
        if not check_gsettings(layout["gsettings"]):
            return False

    return True


def get_layout_name_from_id(layout_id):
    if layout_id == "custom":
        return _("Custom")

    with open(f"{_LAYOUTS_UI}/{layout_id}.json", "r") as file:
        layout_dict = layout_as_dict(file.read())
        return _(layout_dict.get("name", ""))


def get_current_layout():
    for layout in get_available_layouts():
        with open(f"{_LAYOUTS_UI}/{layout}.json", "r") as file:
            if check_layout_dict(layout_as_dict(file.read())):
                return layout.replace(".json", "")
    return "custom"


def get_available_layouts():
    layouts = []
    for layout in os.listdir(_LAYOUTS_UI):
        if layout.endswith(".json"):
            layouts.append(layout.replace(".json", ""))
    return layouts
