# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.

import argparse
import array
import re
import socket
import struct
import subprocess
import sys

import mozinfo
import mozlog

if mozinfo.isLinux:
    import fcntl


class NetworkError(Exception):
    """Exception thrown when unable to obtain interface or IP."""


def _get_logger():
    logger = mozlog.get_default_logger(component='moznetwork')
    if not logger:
        logger = mozlog.unstructured.getLogger('moznetwork')
    return logger


def _get_interface_list():
    """Provides a list of available network interfaces
       as a list of tuples (name, ip)"""
    logger = _get_logger()
    logger.debug('Gathering interface list')
    max_iface = 32  # Maximum number of interfaces(Aribtrary)
    bytes = max_iface * 32
    is_32bit = (8 * struct.calcsize("P")) == 32  # Set Architecture
    struct_size = 32 if is_32bit else 40

    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        names = array.array('B', '\0' * bytes)
        outbytes = struct.unpack('iL', fcntl.ioctl(
            s.fileno(),
            0x8912,  # SIOCGIFCONF
            struct.pack('iL', bytes, names.buffer_info()[0])
        ))[0]
        namestr = names.tostring()
        return [(namestr[i:i + 32].split('\0', 1)[0],
                socket.inet_ntoa(namestr[i + 20:i + 24]))
                for i in range(0, outbytes, struct_size)]

    except IOError:
        raise NetworkError('Unable to call ioctl with SIOCGIFCONF')


def _proc_matches(args, regex):
    """Helper returns the matches of regex in the output of a process created with
    the given arguments"""
    output = subprocess.Popen(args=args,
                              stdout=subprocess.PIPE,
                              stderr=subprocess.STDOUT).stdout.read()
    return re.findall(regex, output)


def _parse_ifconfig():
    """Parse the output of running ifconfig on mac in cases other methods
    have failed"""
    logger = _get_logger()
    logger.debug('Parsing ifconfig')

    # Attempt to determine the default interface in use.
    default_iface = _proc_matches(['route', '-n', 'get', 'default'],
                                  'interface: (\w+)')

    if default_iface:
        addr_list = _proc_matches(['ifconfig', default_iface[0]],
                                  'inet (\d+.\d+.\d+.\d+)')
        if addr_list:
            logger.debug('Default interface: [%s] %s' % (default_iface[0],
                                                         addr_list[0]))
            if not addr_list[0].startswith('127.'):
                return addr_list[0]

    # Iterate over plausible interfaces if we didn't find a suitable default.
    for iface in ['en%s' % i for i in range(10)]:
        addr_list = _proc_matches(['ifconfig', iface],
                                  'inet (\d+.\d+.\d+.\d+)')
        if addr_list:
            logger.debug('Interface: [%s] %s' % (iface, addr_list[0]))
            if not addr_list[0].startswith('127.'):
                return addr_list[0]

    # Just return any that isn't localhost. If we can't find one, we have
    # failed.
    addrs = _proc_matches(['ifconfig'],
                          'inet (\d+.\d+.\d+.\d+)')
    try:
        return [addr for addr in addrs if not addr.startswith('127.')][0]
    except IndexError:
        return None


def get_ip():
    """Provides an available network interface address, for example
       "192.168.1.3".

       A `NetworkError` exception is raised in case of failure."""
    logger = _get_logger()
    try:
        hostname = socket.gethostname()
        try:
            logger.debug('Retrieving IP for %s' % hostname)
            ips = socket.gethostbyname_ex(hostname)[2]
        except socket.gaierror:  # for Mac OS X
            hostname += '.local'
            logger.debug('Retrieving IP for %s' % hostname)
            ips = socket.gethostbyname_ex(hostname)[2]
        if len(ips) == 1:
            ip = ips[0]
        elif len(ips) > 1:
            logger.debug('Multiple addresses found: %s' % ips)
            # no fallback on Windows so take the first address
            ip = ips[0] if mozinfo.isWin else None
        else:
            ip = None
    except socket.gaierror:
        # sometimes the hostname doesn't resolve to an ip address, in which
        # case this will always fail
        ip = None

    if ip is None or ip.startswith("127."):
        if mozinfo.isLinux:
            interfaces = _get_interface_list()
            for ifconfig in interfaces:
                logger.debug('Interface: [%s] %s' % (ifconfig[0], ifconfig[1]))
                if ifconfig[0] == 'lo':
                    continue
                else:
                    return ifconfig[1]
        elif mozinfo.isMac:
            ip = _parse_ifconfig()

    if ip is None:
        raise NetworkError('Unable to obtain network address')

    return ip


def get_lan_ip():
    """Deprecated. Please use get_ip() instead."""
    return get_ip()


def cli(args=sys.argv[1:]):
    parser = argparse.ArgumentParser(
        description='Retrieve IP address')
    structured.commandline.add_logging_group(
        parser,
        include_formatters=structured.commandline.TEXT_FORMATTERS
    )

    args = parser.parse_args()
    structured.commandline.setup_logging(
        'mozversion', args, {'mach': sys.stdout})

    _get_logger().info('IP address: %s' % get_ip())


if __name__ == '__main__':
    cli()
