# coding=utf-8
import sys
import time

from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from logging import StreamHandler

from filewave.log import log
from generic.models.clients import GenericDesktopClient
from ios.apn import APNPriority, send_apn_to_devices
from ios.command_queue import CommandQueueCache
from ios.models import MDMUser


class Command(BaseCommand):

    def add_arguments(self, parser):
        parser.add_argument(
            '--serial',
            dest='serial',
            help='Device serial.',
        )

        parser.add_argument(
            '--serial-file',
            dest='serial_file',
            help='Text file containing serials, one per line.',
        )

        parser.add_argument(
            '--all',
            dest='all_devices',
            action='store_true',
            help='Explicitly target every macOS device. Required as a '
                 'confirmation if neither --serial nor --serial-file is given.',
        )

        parser.add_argument(
            '--dry-run',
            dest='dry_run',
            action='store_true',
            help='List matching devices only. Queue nothing, send no APNs.',
        )

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

        # print to stdout usual log messages
        log.handlers.append(StreamHandler(stream=sys.stdout))

        serial = options['serial']
        serial_file = options['serial_file']
        all_devices = options['all_devices']
        dry_run = options['dry_run']

        if serial and serial_file:
            raise CommandError('Pass only one of --serial or --serial-file, not both.')

        if not serial and not serial_file and not all_devices:
            raise CommandError('Pass --serial, --serial-file, or --all to confirm.')

        qs = GenericDesktopClient.objects.filter(operating_system__type='OSX')

        if serial:
            qs = qs.filter(serial_number=serial)
        elif serial_file:
            with open(serial_file, 'r') as f:
                serials = [line.strip() for line in f if line.strip()]

            if not serials:
                raise CommandError('%s contained no serial numbers.' % serial_file)

            qs = qs.filter(serial_number__in=serials)

        pks = set(qs.values_list('pk', flat=True))

        if not pks:
            self.stdout.write('No matching device.')
            self.stdout.write('Done.')
            return

        if dry_run:
            for serial_number in qs.values_list('serial_number', flat=True).order_by('serial_number'):
                self.stdout.write('[dry-run] Would target device %s.' % serial_number)
            self.stdout.write('[dry-run] %d matching device(s). Nothing queued, no APNs sent.' % len(pks))
            self.stdout.write('Done.')
            return

        # Only the command-queue writes happen inside the transaction. This
        # part is DB-only (no network calls, no sleeps), so wrapping it
        # atomically is safe and cheap: either every device gets its command
        # queued, or none do.
        mdm_users = []
        with transaction.atomic():
            users = MDMUser.get_users_and_command_queue(
                device_pks=pks,
                exclude_users=True,
                #locking=LockingStrategy.FOR_UPDATE_OF_COMMAND_QUEUE,
            )
            if users:
                with CommandQueueCache() as cq_cache:
                    for user in users:
                        mdm_client = getattr(user, 'mdm_client', None)
                        if not mdm_client or not getattr(mdm_client, 'client', None):
                            self.stderr.write(
                                'Skipping MDMUser %s: no linked MDM client.' % user.pk
                            )
                            continue

                        cq = cq_cache.get_for_user(user)
                        self.stdout.write(
                            'Add InstallApplication command to device %s.' % mdm_client.client.serial_number
                        )
                        cq.create_install_fwcld_command()
                        mdm_users.append(user)

        if not mdm_users:
            self.stdout.write('No InstallApplication commands queued.')
            self.stdout.write('Done.')
            return

        self.stdout.write('Queued %d InstallApplication command(s).' % len(mdm_users))

        # APNs are sent after the queuing transaction has committed and
        # outside of any transaction. A failed or slow push here can never
        # undo commands that were already queued successfully above.
        sent = 0
        failed = []
        for user in mdm_users:
            try:
                send_apn_to_devices(priority=APNPriority.PRIORITY_0, mdm_users=[user.pk])
                sent += 1
                self.stdout.write('Sent APN to %s.' % user.pk)
            except Exception as exc:
                failed.append(user.pk)
                self.stderr.write('Failed to send APN to %s: %s' % (user.pk, exc))
            time.sleep(1)

        self.stdout.write('Sent %d APN(s), %d failed.' % (sent, len(failed)))
        if failed:
            self.stdout.write(
                'Failed MDMUser pks (command is already queued for these; '
                'retry the APN or wait for the next natural check-in): %s'
                % ', '.join(str(pk) for pk in failed)
            )

        self.stdout.write('Done.')
