import os.path
import re
import sys
import tempfile
import logging

import spur

from ostack_validator.common import Issue, Mark, MarkedIssue, index, path_relative_to
from ostack_validator.model import *


class NodeClient(object):

    def __init__(self, node_address, username, private_key_file, ssh_port=22):
        super(NodeClient, self).__init__()
        self.shell = spur.SshShell(
            hostname=node_address,
            port=ssh_port,
            username=username,
            private_key_file=private_key_file,
            missing_host_key=spur.ssh.MissingHostKey.accept)

    def run(self, command, *args, **kwargs):
        return self.shell.run(command, allow_error=True, *args, **kwargs)

    def open(self, path, mode='r'):
        return self.shell.open(path, mode)

python_re = re.compile('(/?([^/]*/)*)python[0-9.]*')
host_port_re = re.compile('(\d+\.\d+\.\d+\.\d+):(\d+)')


class OpenstackDiscovery(object):

    def discover(self, initial_nodes, username, private_key):
        "Takes a list of node addresses and returns discovered openstack installation info"
        openstack = Openstack()

        private_key_file = None
        if private_key:
            private_key_file = tempfile.NamedTemporaryFile(suffix='.key')
            private_key_file.write(private_key)
            private_key_file.flush()

        for address in initial_nodes:
            try:
                m = host_port_re.match(address)
                if m:
                    host = m.group(1)
                    port = int(m.group(2))
                else:
                    host = address
                    port = 22
                client = NodeClient(
                    host,
                    ssh_port=port,
                    username=username,
                    private_key_file=private_key_file.name)
                client.run(['echo', 'test'])
            except:
                openstack.report_issue(
                    Issue(
                        Issue.WARNING,
                        "Can't connect to node %s" %
                        address))
                continue

            host = self._discover_node(client)

            if len(host.components) == 0:
                continue

            openstack.add_host(host)

        if len(openstack.hosts) == 0:
            openstack.report_issue(
                Issue(Issue.FATAL, "No OpenStack nodes were discovered"))

        if private_key_file:
            private_key_file.close()

        return openstack

    def _discover_node(self, client):
        hostname = client.run(['hostname']).output.strip()

        host = Host(name=hostname)
        host.id = self._collect_host_id(client)
        host.network_addresses = self._collect_host_network_addresses(client)

        host.add_component(self._collect_keystone_data(client))
        host.add_component(self._collect_nova_api_data(client))
        host.add_component(self._collect_nova_compute_data(client))
        host.add_component(self._collect_nova_scheduler_data(client))
        host.add_component(self._collect_glance_api_data(client))
        host.add_component(self._collect_glance_registry_data(client))
        host.add_component(self._collect_cinder_api_data(client))
        host.add_component(self._collect_cinder_volume_data(client))
        host.add_component(self._collect_cinder_scheduler_data(client))
        host.add_component(self._collect_mysql_data(client))
        host.add_component(self._collect_rabbitmq_data(client))

        return host

    def _find_process(self, client, name):
        processes = self._get_processes(client)
        for line in processes:
            if len(line) > 0 and os.path.basename(line[0]) == name:
                return line

        return None

    def _find_python_process(self, client, name):
        processes = self._get_processes(client)
        for line in processes:
            if len(line) > 0 and (line[0] == name or line[0].endswith('/' + name)):
                return line
            if len(line) > 1 and python_re.match(line[0]) and (line[1] == name or line[1].endswith('/' + name)):
                return line

        return None

    def _find_python_package_version(self, client, package):
        result = client.run(
            ['python', '-c', 'import pkg_resources; version = pkg_resources.get_provider(pkg_resources.Requirement.parse("%s")).version; print(version)' %
             package])

        s = result.output.strip()
        parts = []
        for p in s.split('.'):
            if not p[0].isdigit():
                break

            parts.append(p)

        version = '.'.join(parts)

        return version

    def _get_processes(self, client):
        return (
            [line.split()
             for line in client.run(['ps', '-Ao', 'cmd', '--no-headers']).output.split("\n")]
        )

    def _collect_host_id(self, client):
        ether_re = re.compile('link/ether (([0-9a-f]{2}:){5}([0-9a-f]{2})) ')
        result = client.run(['bash', '-c', 'ip link | grep "link/ether "'])
        macs = []
        for match in ether_re.finditer(result.output):
            macs.append(match.group(1).replace(':', ''))
        return ''.join(macs)

    def _collect_host_network_addresses(self, client):
        ipaddr_re = re.compile('inet (\d+\.\d+\.\d+\.\d+)/\d+')
        addresses = []
        result = client.run(['bash', '-c', 'ip address list | grep "inet "'])
        for match in ipaddr_re.finditer(result.output):
            addresses.append(match.group(1))
        return addresses

    def _permissions_string_to_number(self, s):
        return 0

    def _collect_file(self, client, path):
        ls = client.run(['ls', '-l', '--time-style=full-iso', path])
        if ls.return_code != 0:
            return None

        line = ls.output.split("\n")[0]
        perm, links, owner, group, size, date, time, timezone, name = line.split(
        )
        permissions = self._permissions_string_to_number(perm)

        with client.open(path) as f:
            contents = f.read()

        return FileResource(path, contents, owner, group, permissions)

    def _get_keystone_db_data(self, client, command, env={}):
        result = client.run(['keystone', command], update_env=env)
        if result.return_code != 0:
            return []

        lines = result.output.strip().split("\n")

        columns = []
        last_pos = 0
        l = lines[0]
        while True:
            pos = l.find('+', last_pos + 1)
            if pos == -1:
                break

            columns.append({'start': last_pos + 1, 'end': pos - 1})

            last_pos = pos

        l = lines[1]
        for c in columns:
            c['name'] = l[c['start']:c['end']].strip()

        data = []
        for l in lines[3:-1]:
            d = dict()
            for c in columns:
                d[c['name']] = l[c['start']:c['end']].strip()

            data.append(d)

        return data

    def _collect_keystone_data(self, client):
        keystone_process = self._find_python_process(client, 'keystone-all')
        if not keystone_process:
            return None

        p = index(keystone_process, lambda s: s == '--config-file')
        if p != -1 and p + 1 < len(keystone_process):
            config_path = keystone_process[p + 1]
        else:
            config_path = '/etc/keystone/keystone.conf'

        keystone = KeystoneComponent()
        keystone.version = self._find_python_package_version(
            client, 'keystone')
        keystone.config_files = []
        keystone.config_files.append(self._collect_file(client, config_path))

        token = keystone.config['admin_token']
        host = keystone.config['bind_host']
        if host == '0.0.0.0':
            host = '127.0.0.1'
        port = int(keystone.config['admin_port'])

        keystone_env = {
            'OS_SERVICE_TOKEN': token,
            'OS_SERVICE_ENDPOINT': 'http://%s:%d/v2.0' % (host, port)
        }

        keystone.db = dict()
        keystone.db['tenants'] = self._get_keystone_db_data(
            client, 'tenant-list', env=keystone_env)
        keystone.db['users'] = self._get_keystone_db_data(
            client, 'user-list', env=keystone_env)
        keystone.db['services'] = self._get_keystone_db_data(
            client, 'service-list', env=keystone_env)
        keystone.db['endpoints'] = self._get_keystone_db_data(
            client, 'endpoint-list', env=keystone_env)

        return keystone

    def _collect_nova_api_data(self, client):
        process = self._find_python_process(client, 'nova-api')
        if not process:
            return None

        p = index(process, lambda s: s == '--config-file')
        if p != -1 and p + 1 < len(process):
            config_path = process[p + 1]
        else:
            config_path = '/etc/nova/nova.conf'

        nova_api = NovaApiComponent()
        nova_api.version = self._find_python_package_version(client, 'nova')
        nova_api.config_files = []
        nova_api.config_files.append(self._collect_file(client, config_path))

        paste_config_path = path_relative_to(
            nova_api.config['api_paste_config'],
            os.path.dirname(config_path))
        nova_api.paste_config_file = self._collect_file(
            client, paste_config_path)

        return nova_api

    def _collect_nova_compute_data(self, client):
        process = self._find_python_process(client, 'nova-compute')
        if not process:
            return None

        p = index(process, lambda s: s == '--config-file')
        if p != -1 and p + 1 < len(process):
            config_path = process[p + 1]
        else:
            config_path = '/etc/nova/nova.conf'

        nova_compute = NovaComputeComponent()
        nova_compute.version = self._find_python_package_version(
            client, 'nova')
        nova_compute.config_files = []
        nova_compute.config_files.append(
            self._collect_file(client, config_path))

        return nova_compute

    def _collect_nova_scheduler_data(self, client):
        process = self._find_python_process(client, 'nova-scheduler')
        if not process:
            return None

        p = index(process, lambda s: s == '--config-file')
        if p != -1 and p + 1 < len(process):
            config_path = process[p + 1]
        else:
            config_path = '/etc/nova/nova.conf'

        nova_scheduler = NovaSchedulerComponent()
        nova_scheduler.version = self._find_python_package_version(
            client, 'nova')
        nova_scheduler.config_files = []
        nova_scheduler.config_files.append(
            self._collect_file(client, config_path))

        return nova_scheduler

    def _collect_glance_api_data(self, client):
        process = self._find_python_process(client, 'glance-api')
        if not process:
            return None

        p = index(process, lambda s: s == '--config-file')
        if p != -1 and p + 1 < len(process):
            config_path = process[p + 1]
        else:
            config_path = '/etc/glance/glance-api.conf'

        glance_api = GlanceApiComponent()
        glance_api.version = self._find_python_package_version(
            client, 'glance')
        glance_api.config_files = []
        glance_api.config_files.append(self._collect_file(client, config_path))

        return glance_api

    def _collect_glance_registry_data(self, client):
        process = self._find_python_process(client, 'glance-registry')
        if not process:
            return None

        p = index(process, lambda s: s == '--config-file')
        if p != -1 and p + 1 < len(process):
            config_path = process[p + 1]
        else:
            config_path = '/etc/glance/glance-registry.conf'

        glance_registry = GlanceRegistryComponent()
        glance_registry.version = self._find_python_package_version(
            client, 'glance')
        glance_registry.config_files = []
        glance_registry.config_files.append(
            self._collect_file(client, config_path))

        return glance_registry

    def _collect_cinder_api_data(self, client):
        process = self._find_python_process(client, 'cinder-api')
        if not process:
            return None

        p = index(process, lambda s: s == '--config-file')
        if p != -1 and p + 1 < len(process):
            config_path = process[p + 1]
        else:
            config_path = '/etc/cinder/cinder.conf'

        cinder_api = CinderApiComponent()
        cinder_api.version = self._find_python_package_version(
            client, 'cinder')
        cinder_api.config_files = []
        cinder_api.config_files.append(self._collect_file(client, config_path))

        paste_config_path = path_relative_to(
            cinder_api.config['api_paste_config'],
            os.path.dirname(config_path))
        cinder_api.paste_config_file = self._collect_file(
            client, paste_config_path)

        return cinder_api

    def _collect_cinder_volume_data(self, client):
        process = self._find_python_process(client, 'cinder-volume')
        if not process:
            return None

        p = index(process, lambda s: s == '--config-file')
        if p != -1 and p + 1 < len(process):
            config_path = process[p + 1]
        else:
            config_path = '/etc/cinder/cinder.conf'

        cinder_volume = CinderVolumeComponent()
        cinder_volume.version = self._find_python_package_version(
            client, 'cinder')
        cinder_volume.config_files = []
        cinder_volume.config_files.append(
            self._collect_file(client, config_path))

        rootwrap_config_path = path_relative_to(
            cinder_volume.config['rootwrap_config'],
            os.path.dirname(config_path))
        cinder_volume.rootwrap_config = self._collect_file(
            client, rootwrap_config_path)

        return cinder_volume

    def _collect_cinder_scheduler_data(self, client):
        process = self._find_python_process(client, 'cinder-scheduler')
        if not process:
            return None

        p = index(process, lambda s: s == '--config-file')
        if p != -1 and p + 1 < len(process):
            config_path = process[p + 1]
        else:
            config_path = '/etc/cinder/cinder.conf'

        cinder_scheduler = CinderSchedulerComponent()
        cinder_scheduler.version = self._find_python_package_version(
            client, 'cinder')
        cinder_scheduler.config_files = []
        cinder_scheduler.config_files.append(
            self._collect_file(client, config_path))

        return cinder_scheduler

    def _collect_mysql_data(self, client):
        process = self._find_process(client, 'mysqld')
        if not process:
            return None

        mysqld_version_re = re.compile('mysqld\s+Ver\s(\S+)\s')

        mysql = MysqlComponent()

        version_result = client.run(['mysqld', '--version'])
        m = mysqld_version_re.match(version_result.output)
        mysql.version = m.group(1) if m else 'unknown'

        mysql.config_files = []
        config_locations_result = client.run(
            ['bash', '-c', 'mysqld --help --verbose | grep "Default options are read from the following files in the given order" -A 1'])
        config_locations = config_locations_result.output.strip().split(
            "\n")[-1].split()
        for path in config_locations:
            f = self._collect_file(client, path)
            if f:
                mysql.config_files.append(f)

        return mysql

    def _collect_rabbitmq_data(self, client):
        process = self._find_process(client, 'beam.smp')
        if not process:
            return None

        if ' '.join(process).find('rabbit') == -1:
            return None

        rabbitmq = RabbitMqComponent()
        rabbitmq.version = 'unknown'

        return rabbitmq