from __future__ import unicode_literals

from getpass import getpass
from ncclient.transport.errors import AuthenticationError
from paramiko import AuthenticationException

from django.conf import settings
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction

from dcim.models import Device, InventoryItem, Site, STATUS_ACTIVE


class Command(BaseCommand):
    help = "Update inventory information for specified devices"
    username = settings.NAPALM_USERNAME
    password = settings.NAPALM_PASSWORD

    def add_arguments(self, parser):
        parser.add_argument('-u', '--username', dest='username', help="Specify the username to use")
        parser.add_argument('-p', '--password', action='store_true', default=False, help="Prompt for password to use")
        parser.add_argument('-s', '--site', dest='site', action='append',
                            help="Filter devices by site (include argument once per site)")
        parser.add_argument('-n', '--name', dest='name', help="Filter devices by name (regular expression)")
        parser.add_argument('--full', action='store_true', default=False, help="For inventory update for all devices")
        parser.add_argument('--fake', action='store_true', default=False, help="Do not actually update database")

    def handle(self, *args, **options):

        def create_inventory_items(inventory_items, parent=None):
            for item in inventory_items:
                i = InventoryItem(device=device, parent=parent, name=item['name'], part_id=item['part_id'],
                                  serial=item['serial'], discovered=True)
                i.save()
                create_inventory_items(item.get('items', []), parent=i)

        # Credentials
        if options['username']:
            self.username = options['username']
        if options['password']:
            self.password = getpass("Password: ")

        # Attempt to inventory only active devices
        device_list = Device.objects.filter(status=STATUS_ACTIVE)

        # --site: Include only devices belonging to specified site(s)
        if options['site']:
            sites = Site.objects.filter(slug__in=options['site'])
            if sites:
                site_names = [s.name for s in sites]
                self.stdout.write("Running inventory for these sites: {}".format(', '.join(site_names)))
            else:
                raise CommandError("One or more sites specified but none found.")
            device_list = device_list.filter(site__in=sites)

        # --name: Filter devices by name matching a regex
        if options['name']:
            device_list = device_list.filter(name__iregex=options['name'])

        # --full: Gather inventory data for *all* devices
        if options['full']:
            self.stdout.write("WARNING: Running inventory for all devices! Prior data will be overwritten. (--full)")

        # --fake: Gathering data but not updating the database
        if options['fake']:
            self.stdout.write("WARNING: Inventory data will not be saved! (--fake)")

        device_count = device_list.count()
        self.stdout.write("** Found {} devices...".format(device_count))

        for i, device in enumerate(device_list, start=1):

            self.stdout.write("[{}/{}] {}: ".format(i, device_count, device.name), ending='')

            # Skip inactive devices
            if not device.status:
                self.stdout.write("Skipped (not active)")
                continue

            # Skip devices without primary_ip set
            if not device.primary_ip:
                self.stdout.write("Skipped (no primary IP set)")
                continue

            # Skip devices which have already been inventoried if not doing a full update
            if device.serial and not options['full']:
                self.stdout.write("Skipped (Serial: {})".format(device.serial))
                continue

            RPC = device.get_rpc_client()
            if not RPC:
                self.stdout.write("Skipped (no RPC client available for platform {})".format(device.platform))
                continue

            # Connect to device and retrieve inventory info
            try:
                with RPC(device, self.username, self.password) as rpc_client:
                    inventory = rpc_client.get_inventory()
            except KeyboardInterrupt:
                raise
            except (AuthenticationError, AuthenticationException):
                self.stdout.write("Authentication error!")
                continue
            except Exception as e:
                self.stdout.write("Error: {}".format(e))
                continue

            if options['verbosity'] > 1:
                self.stdout.write("")
                self.stdout.write("\tSerial: {}".format(inventory['chassis']['serial']))
                self.stdout.write("\tDescription: {}".format(inventory['chassis']['description']))
                for item in inventory['items']:
                    self.stdout.write("\tItem: {} / {} ({})".format(item['name'], item['part_id'],
                                                                    item['serial']))
            else:
                self.stdout.write("{} ({})".format(inventory['chassis']['description'], inventory['chassis']['serial']))

            if not options['fake']:
                with transaction.atomic():
                    # Update device serial
                    if device.serial != inventory['chassis']['serial']:
                        device.serial = inventory['chassis']['serial']
                        device.save()
                    InventoryItem.objects.filter(device=device, discovered=True).delete()
                    create_inventory_items(inventory.get('items', []))

        self.stdout.write("Finished!")
