"""Operating system-related utilities for the IoT"""

import configparser
import logging
import netifaces
import re
import secrets
import socket
import subprocess
import sys
import time

from functools import cache
from pathlib import Path
from platform import system, release

from odoo import release as odoo_release

_logger = logging.getLogger(__name__)


IOT_SYSTEM = system()

IOT_RPI_CHAR, IOT_WINDOWS_CHAR, IOT_TEST_CHAR = "L", "W", "T"

IS_WINDOWS = IOT_SYSTEM[0] == IOT_WINDOWS_CHAR
IS_RPI = 'rpi' in release()
IS_TEST = not IS_RPI and not IS_WINDOWS
"""IoT system "Test" correspond to any non-Raspberry Pi nor windows system.
Expected to be Linux or macOS used locally for development purposes."""

IOT_CHAR = IOT_RPI_CHAR if IS_RPI else IOT_WINDOWS_CHAR if IS_WINDOWS else IOT_TEST_CHAR
"""IoT system character used in the identifier and version.
- 'L' for Raspberry Pi
- 'W' for Windows
- 'T' for Test (non-Raspberry Pi nor Windows)"""

if IS_RPI:
    def rpi_only(function):
        """Decorator to check if the system is raspberry pi before running the function."""
        return function
else:
    def rpi_only(_):
        """No-op decorator for non raspberry pi systems."""
        return lambda *args, **kwargs: None


def path_file(*args) -> Path:
    """Return the path to the file from `/home/pi` on IoT Box
    or from `odoo` folder on Virtual IoT Box.

    :return: The path to the file
    """
    return Path(sys.path[0]).parent.joinpath(*args)


def git(*args):
    """Run a git command with the given arguments, taking system
    into account.

    :param args: list of arguments to pass to git
    """
    git_executable = 'git' if IS_RPI else path_file('git', 'cmd', 'git.exe')
    command = [git_executable, f'--work-tree={path_file("odoo")}', f'--git-dir={path_file("odoo", ".git")}', *args]

    p = subprocess.run(command, stdout=subprocess.PIPE, text=True, check=False)
    if p.returncode == 0:
        return p.stdout.strip()
    return None


def pip(*args):
    """Run a pip install command with the given arguments, taking
    system into account.

    :param args: list of arguments to pass to pip install
    :return: True if the command was successful, False otherwise
    """
    python_executable = [] if IS_RPI else [path_file('python', 'python.exe'), '-m']
    command = [*python_executable, 'pip', 'install', *args]

    if IS_RPI:
        command.append('--break-system-package')

    p = subprocess.run(command, stdout=subprocess.PIPE, check=False)
    return p.returncode == 0


@cache
def get_version(detailed_version=False):
    if IS_RPI:
        with open('/var/odoo/iotbox_version', encoding='utf-8') as f:
            image_version = f.readline().strip()
    elif IS_WINDOWS:
        version_path = path_file('VERSION')
        if not version_path.exists():
            image_version = '23.11'
        else:
            with version_path.open(encoding='utf-8') as f:
                image_version = f.readline().strip()
    else:
        image_version = 'test'

    version = IOT_CHAR + image_version
    if detailed_version:
        # Note: on Windows IoT, the `release.version` finish with the build date
        version += f"-{odoo_release.version}"
        if IS_RPI:
            commit_hash = git("rev-parse", "--short", "HEAD")
            version += f'#{commit_hash or "unknown"}'

    return version


def update_conf(values: dict, section: str = "iot.box"):
    """Update odoo.conf with the given key and value.

    :param values: key-value pairs to update the config with.
    :param section: The section to update the key-value pairs in (Default: `iot.box`).
    """
    _logger.debug("Updating odoo.conf with values: %s", values)
    conf = get_conf()

    if not conf.has_section(section):
        _logger.debug("Creating new section '%s' in odoo.conf", section)
        conf.add_section(section)

    for key, value in values.items():
        conf.set(section, key, value) if value else conf.remove_option(section, key)

    with open(path_file("odoo.conf"), "w", encoding='utf-8') as f:
        conf.write(f)


def get_conf(key: str | None = None, section: str = "iot.box"):
    """Get the value of the given key from odoo.conf, or the full config if no key is provided.

    :param key: The key to get the value of.
    :param section: The section to get the key from (Default: `iot.box`).
    :return: The value of the key provided or `None` if it doesn't exist, or full conf object if no key is provided.
    """
    conf = configparser.RawConfigParser()
    conf.read(path_file("odoo.conf"))

    return conf.get(section, key, fallback=None) if key else conf  # Return the key's value or the configparser object


def _get_identifier():
    if IS_RPI:
        with open('/sys/firmware/devicetree/base/serial-number', encoding='utf-8') as f:
            return f.readline().strip("\x00")
    elif IS_TEST:
        return 'test_identifier'

    # On Windows, get motherboard's uuid (serial number isn't reliable as it's not always present)
    command = ['powershell', '-Command', "(Get-CimInstance Win32_ComputerSystemProduct).UUID"]
    p = subprocess.run(command, stdout=subprocess.PIPE, check=False)
    identifier = get_conf('generated_identifier')  # Fallback identifier if windows does not return mb UUID
    if p.returncode == 0 and p.stdout.decode().strip():
        return p.stdout.decode().strip()

    _logger.error("Failed to get Windows IoT serial number, defaulting to a random identifier")
    if not identifier:
        identifier = secrets.token_hex()
        update_conf({'generated_identifier': identifier})

    return identifier


def _get_system_uptime():
    if not IS_RPI:
        return 0
    with open("/proc/uptime", encoding='utf-8') as f:
        return float(f.readline().split()[0])


def is_remote_debug_enabled():
    """Check if the tailscale connection is up on the IoT Box"""
    p = subprocess.run(['tailscale', 'status'], check=False)
    return p.returncode == 0


def toggle_remote_debug(auth_key=""):
    """Enable/disable remote connection to the IoT Box using tailscale.

    :param auth_key: The auth key to use to enable remote access.
    If empty, remote access will be disabled.
    """
    _logger.info("%s remote access", 'enabling' if auth_key else 'disabling')

    hostname = get_conf('remote_server') or 'iotbox'
    server_url = re.sub(r'^https?://|/|:', '', hostname + "-" + IOT_IDENTIFIER)
    args = ['sudo', 'tailscale', 'up' if auth_key else 'logout']
    if auth_key:
        args.extend([f'--auth-key={auth_key.strip()}', f'--hostname={server_url}'])
    p = subprocess.run(args, check=False)
    if auth_key and p.returncode == 0:
        # Tailscale stores its state in /var, we need to copy it in the root filesystem to persist after reboot
        subprocess.run(
            ['sudo', 'cp', '-r', '/var/lib/tailscale/', '/root_bypass_ramdisks/var/lib/tailscale/'], check=False
        )
        return True
    return False


@rpi_only
def generate_password():
    """Resets (pi) user password generating a new random one.

    :return: The new generated password
    """
    password = secrets.token_urlsafe(16)
    try:
        subprocess.run(['sudo', 'chpasswd'], input=f"pi:{password}", text=True, check=True)
        return password
    except subprocess.CalledProcessError as e:
        _logger.exception("Failed to generate password: %s", e.output)
        return 'Error: Check IoT log'


def get_ip():
    """Get the local IP address of the IoT Box by creating
    a dummy connection to the gateway or Google DNS.
    """
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    try:
        s.connect((get_gateway() or '8.8.8.8', 1))  # Google DNS
        return s.getsockname()[0]
    except OSError as e:
        _logger.warning("Could not get local IP address: %s", e)
        return None
    finally:
        s.close()


def get_mac_address():
    interfaces = netifaces.interfaces()
    for interface in map(netifaces.ifaddresses, interfaces):
        if interface.get(netifaces.AF_INET):
            addr = interface.get(netifaces.AF_LINK)[0]['addr']
            if addr != '00:00:00:00:00:00':
                return addr
    return None


NGINX_PATH = path_file('nginx')

if IS_WINDOWS and NGINX_PATH:
    def start_nginx_server():
        _logger.info('Start Nginx server: %s\\nginx.exe', NGINX_PATH)
        subprocess.Popen([str(NGINX_PATH / 'nginx.exe')], cwd=str(NGINX_PATH))
elif IS_RPI:
    def start_nginx_server():
        subprocess.check_call(["sudo", "service", "nginx", "restart"])
else:
    def start_nginx_server():
        pass


def mtr(host):
    """Run mtr command to the given host to get both
    packet loss (%) and average latency (ms).

    Note: we use ``-4`` in order to force IPv4, to avoid
    empty results on IPv6 networks.

    :param host: The host to ping.
    :return: A tuple of (packet_loss, avg_latency) or (None, None) if the command failed.
    """
    if IS_WINDOWS:
        return None, None

    # sudo is required for probe interval < 1s, which almost divides execution time by 2
    command = ["sudo", "mtr", "-r", "-C", "--no-dns", "-c", "3", "-i", "0.2", "-4", "-G", "1", host]
    p = subprocess.run(command, stdout=subprocess.PIPE, text=True, check=False)
    if p.returncode != 0:
        return None, None

    output = p.stdout.strip()
    try:
        last_line = output.splitlines()[-1].split(",")
        return float(last_line[6]), float(last_line[10])
    except (IndexError, ValueError):
        return None, None


def get_gateway():
    """Get the router IP address (default gateway)

    :return: The IP address of the default gateway or None if it can't be determined
    """
    gws = netifaces.gateways()
    default = gws.get("default", {})
    gw = default.get(netifaces.AF_INET)
    if gw:
        return gw[0]
    return None


IOT_IDENTIFIER = _get_identifier()
ODOO_START_TIME = time.monotonic()
SYSTEM_START_TIME = ODOO_START_TIME - _get_system_uptime()
