#!/usr/bin/env python
"""hos-legacymounts mounts module: Handle bind mounts with system mountpoints dependencies"""

import os
import re
import subprocess
import time

from hos_legacymounts import exceptions

def do_bindmount(source, target, timeout=0):
    """Handle a bind mount satisfying the optional system mountpoints dependencies.

    This is the main function of this module handling a bind mount. It behaves
    as a glorified `mount` functionality while also taking care of its
    components filesystem dependencies. This means that before handling a bind
    mount, this function makes sure that if the components are meant to be on a
    filesystem that is not yet availble, it bails out or waits for the
    respective mountpoints with a timeout. The actual handling of the dependent
    mounts is not in the scope of this tool. Handling these bind moounts with
    the above described extra checks, avoids shadowing and also bind mounting
    on unwanted filesystems.

    Parameters
    ----------
    source : str
        The source of the bind mountpoint.
    target : str
        The targer of the bind mountpoint.
    timeout : int, optional
        Define a timeout for the polling operations - eg. waiting for
        filesystem label.

    Returns
    -------
    bool
        True, if the bind mount could be completed, otherwise False.

    """
    # If the target is already mounted we bail out.
    if is_mounted_proc(target):
        return True

    for path in [source, target]:
        # If the source/target matches a mountpoint of a system label, we check
        # that it is on the same device as the associated device with
        # respective system label.
        path_fslabel = get_fslabel(path)
        if path_fslabel and not check_mountpoint_label(path, path_fslabel, timeout=timeout):
            return False
        # Also fill the path in case of a new bind mount path
        if not os.path.exists(path):
            os.makedirs(path)

    # Run the actual mount
    process = subprocess.Popen(['mount', '--bind', source, target], stdout=subprocess.PIPE,
                               stderr=subprocess.PIPE, universal_newlines=True)
    process.communicate()
    if process.returncode != 0:
        return False

    return True

def check_mountpoint_label(mnt, label, timeout=0):
    """Check if a path is on a filesystem with a specific label.

    Parameters
    ----------
    mnt : str
        Path to be checked as a mount point.
    label : str
        The label name of the filesystem on which mnt path should reside.
    timeout : int, optional
        Define a timeout for the polling operations - eg. waiting for
        filesystem label.

    Returns
    -------
    bool
        True, mnt is a path on filesystem with a specific label, otherwise
        False.

    Raises
    ------
    BindMountTimeout
        When the timeout is specified, this exception is raised if the mnt path
        didn't end up on a device with the requested label.

    """
    if timeout:
        start_time = time.time()
    else:
        start_time = 0

    # Get device maj:min of the block device associated to label
    label_majmin = get_label_majmin(label, start_time, timeout)

    # Get device maj:min of the block device associated to mnt
    # When mnt doesn't exist, follow the path until we hit an existing entry.
    # This helps in supporting paths that are not yet created.
    mnt = os.path.abspath(mnt)
    while mnt != '/':
        if not os.path.exists(mnt):
            mnt = os.path.dirname(mnt)
        else:
            break
    while True:
        mnt_dev = os.stat(mnt).st_dev
        mnt_majmin = '%s:%s' % (os.major(mnt_dev), os.minor(mnt_dev))
        if label_majmin == mnt_majmin:
            return True
        if timeout:
            if time.time() - start_time < timeout:
                time.sleep(1)
                continue
            else:
                raise exceptions.BindMountTimeout(
                    "Timeout while waiting for %s to get mounted in %s." % (label, mnt))
        return False

def get_label_majmin(label, start_time=0, timeout=0):
    """Returns the device major and minor of the device which has a specific
    filesystem label.

    Parameters
    ----------
    label : str
        The label name of the filesystem on the device we want to return the
        major/minor.
    start_time : int, optional
        When during a timeout transaction, have an initial value of the timer.
    timeout : int, optional
        Define a timeout for the polling operations - eg. waiting for
        filesystem label.

    Returns
    -------
    str
        Returns the device major and minor as a string in the following format:
        "major:minor".

    Raises
    ------
    BindMountTimeout
        When the timeout is specified, this exception is raised if the
        filesystem udev label symlink was not created in due time.

    """
    # Find the udev label symlink
    label_link = '/dev/disk/by-label/' + label
    while True:
        if os.path.islink(label_link):
            break
        if timeout:
            if time.time() - start_time < timeout:
                time.sleep(1)
                continue
            else:
                raise exceptions.BindMountTimeout(
                    "Timeout while waiting for %s udev label." % label)
        raise exceptions.NoSuchFSLabel("Couldn't find the %s fs label." % label)
    # Found the label symlink - proceed to find major:minor for the associated device
    label_dev = os.path.realpath(label_link).split('/')[-1]
    with open('/sys/class/block/%s/dev' % label_dev) as label_sys_dev:
        return label_sys_dev.read().strip()

def is_mounted_proc(path):
    """Check if a path is a mount point.

    Parameters
    ----------
    path : str
        Path to check.

    Returns
    -------
    bool
        True, if path is a mountpoint, otherwise False.

    """
    if not path:
        return False
    path = os.path.abspath(path)
    with open('/proc/mounts', 'r') as mounts_fd:
        for line in mounts_fd.readlines():
            if line.split()[1] == path:
                return True
    return False

def get_fslabel(target):
    """Compute the filesystem label based on a system path.

    This method has HanoverOS knowledge behavior. A system partition is a
    partition that has a label in the following format: "hos-*". These
    partitions are mounted under the following paths: /mnt/<label>. Under this
    system-specific interface, from a target path, we can infer the associated
    filesystem label.

    Parameters
    ----------
    target : str
        Path to compute the system filesystem label for.

    Returns
    -------
    str
        The associated filesystem label.

    """
    match = re.search(r'^\/mnt\/(hos-[a-zA-Z0-9-_]+)\/?.*', target)
    if match:
        return match.group(1)
    return ''
