# Copyright (C) 2009 Canonical
#
# Authors:
#  Michael Vogt
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; version 3.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA

import aptsources.distro
import ctypes
from datetime import datetime
import distro_info
from functools import wraps
import gi
gi.require_version("Gtk", "4.0")
from gi.repository import Gio, Gtk, Gdk, GdkX11, GObject
import json
import os
import subprocess

import logging
LOG=logging.getLogger(__name__)

import time

UA_STATUS_JSON = "/var/lib/ubuntu-advantage/status.json"

def setup_ui(self, path, domain):
    # setup ui
    self.builder = Gtk.Builder()
    # In GTK4, set_current_object requires a GObject; dialogs here may be plain Python classes
    if isinstance(self, GObject.Object):
        self.builder.set_current_object(self)
    self.builder.set_translation_domain(domain)
    self.builder.add_from_file(path)
    for o in self.builder.get_objects():
        if issubclass(type(o), Gtk.Buildable):
            setattr(self, o.get_buildable_id(), o)
        else:
            logging.debug(f"can not get name for object '{o}'")

def has_gnome_online_accounts():
    return Gio.DesktopAppInfo.new('gnome-online-accounts-panel.desktop') != None

def is_current_distro_lts():
    distro = aptsources.distro.get_distro()
    di = distro_info.UbuntuDistroInfo()
    return di.is_lts(distro.codename)

def is_current_distro_supported():
    distro = aptsources.distro.get_distro()
    di = distro_info.UbuntuDistroInfo()
    return distro.codename in di.supported(datetime.now().date())

def current_distro():
    distro = aptsources.distro.get_distro()
    di = distro_info.UbuntuDistroInfo()
    releases = di.get_all(result="object")
    for release in releases:
        if release.series == distro.codename:
            return release


def get_ua_status():
    """Return a dict of all UA status information or empty dict on error."""
    # status.json will exist on any attached system. It will also be created
    # by the systemd timer ua-timer which will update UA_STATUS_JSON every 12
    # hours to reflect current status of UA subscription services.
    # Invoking `pro status` with subp will result in a network call to
    # contracts.canonical.com which could raise Timeouts on network limited
    # machines. So, prefer the status.json file when possible.
    status_json = ""
    if os.path.exists(UA_STATUS_JSON):
        with open(UA_STATUS_JSON) as stream:
            status_json = stream.read()
    else:
        try:
            # Success writes UA_STATUS_JSON
            result = subprocess.run(
                ['pro', 'status', '--format=json'], stdout=subprocess.PIPE
            )
        except Exception as e:
            print("Failed to run `pro status`:\n%s" % e)
            return {}
        if result.returncode != 0:
            print(
                "Ubuntu Pro client returned code %d" % result.returncode
            )
            return {}
        status_json = result.stdout
    if not status_json:
        print(
            "Warning: no Ubuntu Pro Client status found."
            " Is ubuntu-pro-client installed?"
        )
        return {}
    try:
        status = json.loads(status_json)
    except json.JSONDecodeError as e:
        print("Failed to parse ubuntu advantage client JSON:\n%s" % e)
        return {}
    if status.get("_schema_version", "0.1") != "0.1":
        print(
            "Pro status schema version change: %s" % status["_schema_version"]
        )
    return status


def get_ua_service_status(service_name='esm-infra', status=None):
    """Get service availability and status for a specific UA service.

    Return a tuple (available, service_status).
      :boolean available: set True when either:
        - attached contract is entitled to the service
        - unattached machine reports service "availability" as "yes"
      :str service_status: will be one of the following:
        - "disabled" when the service is available and applicable but not
          active
        - "enabled" when the service is available and active
        - "n/a" when the service is not applicable to the environment or not
          entitled for the attached contract
    """
    if not status:
        status = get_ua_status()
    # Assume unattached on empty status dict
    available = False
    service_status = "n/a"
    for service in status.get("services", []):
        if service.get("name") != service_name:
            continue
        if "available" in service:
            available = bool("yes" == service["available"])
        if "status" in service:
            service_status = service["status"]  # enabled, disabled or n/a
    return (available, service_status)


def retry(exceptions, tries=10, delay=0.1, backoff=2):
    """
    Retry calling the decorated function using an exponential backoff.

    Args:
        exceptions: The exception to check. may be a tuple of
            exceptions to check.
        tries: Number of times to try (not retry) before giving up.
        delay: Initial delay between retries in seconds.
        backoff: Backoff multiplier (e.g. value of 2 will double the delay
            each retry).
    """
    def deco_retry(f):

        @wraps(f)
        def f_retry(*args, **kwargs):
            mtries, mdelay = tries, delay
            while mtries > 1:
                try:
                    return f(*args, **kwargs)
                except exceptions as e:
                    msg = '{}, Retrying in {} seconds...'.format(e, mdelay)
                    logging.warning(msg)
                    time.sleep(mdelay)
                    mtries -= 1
                    mdelay *= backoff
            return f(*args, **kwargs)

        return f_retry  # true decorator

    return deco_retry


def maybe_hint_dialog_window(window):
    """
    Workaround for GTK4 removal of set_type_hint.
    Manually sets _NET_WM_WINDOW_TYPE_DIALOG on X11.
    See: https://discourse.gnome.org/t/replacing-gtk-window-set-type-hint-in-gtk4
    """
    display = Gdk.Display.get_default()
    # Return early if we're not running on X11
    if not isinstance(display, GdkX11.X11Display):
        return

    def _apply_x11_hint(win):
        native = win.get_native()
        surface = native.get_surface()
        xid = surface.get_xid()

        # Load X11 (Allow this to raise OSError if missing, per review)
        libx11 = ctypes.CDLL("libX11.so.6")

        XA_ATOM = 4
        PropModeReplace = 0
        libx11.XOpenDisplay.restype = ctypes.c_void_p
        libx11.XInternAtom.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_int]
        libx11.XInternAtom.restype = ctypes.c_ulong

        # Open a new connection to avoid any pointer compatibility
        # issues between gi and ctypes.
        display_ptr = libx11.XOpenDisplay(None)
        if not display_ptr:
            return

        try:
            atom_type = libx11.XInternAtom(display_ptr, b"_NET_WM_WINDOW_TYPE", 0)
            atom_dialog = libx11.XInternAtom(display_ptr, b"_NET_WM_WINDOW_TYPE_DIALOG", 0)
            data = (ctypes.c_ulong * 1)(atom_dialog)

            # XChangeProperty(display, w, property, type, format, mode, data, nelements)
            libx11.XChangeProperty(
                display_ptr,
                xid,
                atom_type,
                XA_ATOM,
                32,
                PropModeReplace,
                ctypes.cast(data, ctypes.POINTER(ctypes.c_ubyte)),
                1
            )
            libx11.XFlush(display_ptr)
        finally:
            libx11.XCloseDisplay(display_ptr)

    # Apply hints immediately if already realised, otherwise wait for signal
    if window.get_realized():
        _apply_x11_hint(window)
    else:
        window.connect('realize', _apply_x11_hint)
