# 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
if mozinfo.isWin:
    import os


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(arbitrary)
    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", b"\0" * bytes)
        outbytes = struct.unpack(
            "iL",
            fcntl.ioctl(
                s.fileno(),
                0x8912,  # SIOCGIFCONF
                struct.pack("iL", bytes, names.buffer_info()[0]),
            ),
        )[0]
        namestr = names.tobytes()
        return [
            (
                namestr[i : i + 32].split(b"\0", 1)[0].decode(),
                socket.inet_ntoa(namestr[i + 20 : i + 24]),
            )
            for i in range(0, outbytes, struct_size)
        ]

    except OSError:
        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,
        universal_newlines=True,
    ).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"], r"interface: (\w+)"
    )

    if default_iface:
        addr_list = _proc_matches(
            ["ifconfig", default_iface[0]], r"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], r"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"], r"inet (\d+.\d+.\d+.\d+)")
    try:
        return [addr for addr in addrs if not addr.startswith("127.")][0]
    except IndexError:
        return None


def _parse_powershell():
    logger = _get_logger()
    logger.debug("Parsing Get-NetIPAdress output via PowerShell")

    try:
        cmd = os.path.join(
            os.environ.get("SystemRoot", "C:\\WINDOWS"),
            "system32",
            "windowspowershell",
            "v1.0",
            "powershell.exe",
        )
        output = subprocess.check_output(
            [
                cmd,
                "(Get-NetIPAddress -AddressFamily IPv4 -AddressState Preferred | Format-List -Property IPAddress)",
            ]
        ).decode("ascii")
        ips = re.findall(r"IPAddress : (\d+.\d+.\d+.\d+)", output)
        for ip in ips:
            logger.debug("IPAddress: %s" % ip)
            if not ip.startswith("127."):
                return ip
        return None
    except FileNotFoundError:
        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)
            ip = 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()
        elif mozinfo.isWin:
            ip = _parse_powershell()

    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")
    mozlog.commandline.add_logging_group(
        parser, include_formatters=mozlog.commandline.TEXT_FORMATTERS
    )

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

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


if __name__ == "__main__":
    cli()
