# This file is part of Ansible
#
# Ansible 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, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible 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 Ansible.  If not, see <http://www.gnu.org/licenses/>.

from __future__ import annotations

import re
import time

from ansible.module_utils.common.text.converters import to_text

from ansible.module_utils.facts.hardware.base import Hardware, HardwareCollector
from ansible.module_utils.facts import timeout

from ansible.module_utils.facts.utils import get_file_content, get_mount_size
from ansible.module_utils.facts.sysctl import get_sysctl


class OpenBSDHardware(Hardware):
    """
    OpenBSD-specific subclass of Hardware. Defines memory, CPU and device facts:
    - memfree_mb
    - memtotal_mb
    - swapfree_mb
    - swaptotal_mb
    - processor (a list)
    - processor_cores
    - processor_count
    - processor_speed
    - uptime_seconds

    In addition, it also defines number of DMI facts and device facts.
    """
    platform = 'OpenBSD'

    def populate(self, collected_facts=None):
        hardware_facts = {}
        self.sysctl = get_sysctl(self.module, ['hw'])

        hardware_facts.update(self.get_processor_facts())
        hardware_facts.update(self.get_memory_facts())
        hardware_facts.update(self.get_device_facts())
        hardware_facts.update(self.get_dmi_facts())
        hardware_facts.update(self.get_uptime_facts())

        # storage devices notoriously prone to hang/block so they are under a timeout
        try:
            hardware_facts.update(self.get_mount_facts())
        except timeout.TimeoutError:
            pass

        return hardware_facts

    @timeout.timeout()
    def get_mount_facts(self):
        mount_facts = {}

        mount_facts['mounts'] = []
        fstab = get_file_content('/etc/fstab')
        if fstab:
            for line in fstab.splitlines():
                if line.startswith('#') or line.strip() == '':
                    continue
                fields = re.sub(r'\s+', ' ', line).split()
                if fields[1] == 'none' or fields[3] == 'xx':
                    continue
                mount_statvfs_info = get_mount_size(fields[1])
                mount_info = {'mount': fields[1],
                              'device': fields[0],
                              'fstype': fields[2],
                              'options': fields[3]}
                mount_info.update(mount_statvfs_info)
                mount_facts['mounts'].append(mount_info)
        return mount_facts

    def get_memory_facts(self):
        memory_facts = {}
        # Get free memory. vmstat output looks like:
        #  procs    memory       page                    disks    traps          cpu
        #  r b w    avm     fre  flt  re  pi  po  fr  sr wd0 fd0  int   sys   cs us sy id
        #  0 0 0  47512   28160   51   0   0   0   0   0   1   0  116    89   17  0  1 99
        rc, out, err = self.module.run_command("/usr/bin/vmstat")
        if rc == 0:
            memory_facts['memfree_mb'] = int(out.splitlines()[-1].split()[4]) // 1024
            memory_facts['memtotal_mb'] = int(self.sysctl['hw.physmem']) // 1024 // 1024

        # Get swapctl info. swapctl output looks like:
        # total: 69268 1K-blocks allocated, 0 used, 69268 available
        # And for older OpenBSD:
        # total: 69268k bytes allocated = 0k used, 69268k available
        rc, out, err = self.module.run_command("/sbin/swapctl -sk")
        if rc == 0:
            swaptrans = {ord(u'k'): None,
                         ord(u'm'): None,
                         ord(u'g'): None}
            data = to_text(out, errors='surrogate_or_strict').split()
            memory_facts['swapfree_mb'] = int(data[-2].translate(swaptrans)) // 1024
            memory_facts['swaptotal_mb'] = int(data[1].translate(swaptrans)) // 1024

        return memory_facts

    def get_uptime_facts(self):
        # On openbsd, we need to call it with -n to get this value as an int.
        sysctl_cmd = self.module.get_bin_path('sysctl')
        if sysctl_cmd is None:
            return {}

        cmd = [sysctl_cmd, '-n', 'kern.boottime']

        rc, out, err = self.module.run_command(cmd)

        if rc != 0:
            return {}

        kern_boottime = out.strip()
        if not kern_boottime.isdigit():
            return {}

        return {
            'uptime_seconds': int(time.time() - int(kern_boottime)),
        }

    def get_processor_facts(self):
        cpu_facts = {}
        processor = []
        for i in range(int(self.sysctl['hw.ncpuonline'])):
            processor.append(self.sysctl['hw.model'])

        cpu_facts['processor'] = processor
        # The following is partly a lie because there is no reliable way to
        # determine the number of physical CPUs in the system. We can only
        # query the number of logical CPUs, which hides the number of cores.
        # On amd64/i386 we could try to inspect the smt/core/package lines in
        # dmesg, however even those have proven to be unreliable.
        # So take a shortcut and report the logical number of processors in
        # 'processor_count' and 'processor_cores' and leave it at that.
        cpu_facts['processor_count'] = self.sysctl['hw.ncpuonline']
        cpu_facts['processor_cores'] = self.sysctl['hw.ncpuonline']

        return cpu_facts

    def get_device_facts(self):
        device_facts = {}
        devices = []
        devices.extend(self.sysctl['hw.disknames'].split(','))
        device_facts['devices'] = devices

        return device_facts

    def get_dmi_facts(self):
        dmi_facts = {}
        # We don't use dmidecode(8) here because:
        # - it would add dependency on an external package
        # - dmidecode(8) can only be ran as root
        # So instead we rely on sysctl(8) to provide us the information on a
        # best-effort basis. As a bonus we also get facts on non-amd64/i386
        # platforms this way.
        sysctl_to_dmi = {
            'hw.product': 'product_name',
            'hw.version': 'product_version',
            'hw.uuid': 'product_uuid',
            'hw.serialno': 'product_serial',
            'hw.vendor': 'system_vendor',
        }

        for mib in sysctl_to_dmi:
            if mib in self.sysctl:
                dmi_facts[sysctl_to_dmi[mib]] = self.sysctl[mib]

        return dmi_facts


class OpenBSDHardwareCollector(HardwareCollector):
    _fact_class = OpenBSDHardware
    _platform = 'OpenBSD'
