import logging
from IPy import IPint
from typing import Any, Dict

from django.conf import settings
from django.db import connection

from staff.celery_app import app
from staff.lib import requests
from staff.lib.db import atomic
from staff.lib.tasks import LockedTask

from staff.whistlah.models import OfficeNet, StaffLastOffice, NOCOffice
from staff.whistlah.utils import (
    parse_postgres_activity,
    round_parse_result_to_minute,
    remove_already_existing_activities,
    replace_login_with_id,
)
from staff.whistlah.postgres import query

logger = logging.getLogger(name=__name__)


@app.task(ignore_result=True)
class UpdatesOffices(LockedTask):
    """Updates offices ip addresses."""

    @atomic
    def locked_run(self):
        networklist = self.get_nets_for_offices()
        if not networklist:
            logger.warning(msg='Empty office-net list')
            return

        self.clear_old_nets_data()
        self.parse_networklist(networklist=networklist)

    def get_nets_for_offices(self):
        url = settings.RACKTABLES_URL + '/networklist.php'
        query_params = {'report': 'staffnets-by-POI'}
        try:
            response = requests.get(url, params=query_params, timeout=0.4)
            return response.content.splitlines() if response.ok else []
        except requests.Timeout:
            return []

    def clear_old_nets_data(self):
        OfficeNet.objects.all().delete()

    def parse_networklist(self, networklist):
        noc_to_office = {
            no['noc_office_id']: (no['office_id'], no['office__name'])
            for no in NOCOffice.objects.values('noc_office_id', 'office_id', 'office__name')
        }
        for net, noc_office_id in (row.decode('utf-8').split(',', 1) for row in networklist):
            office_data = noc_to_office.get(int(noc_office_id))
            if not office_data:
                continue

            try:
                first, last = IPint(net).net(), IPint(net).broadcast()
            except ValueError:
                logger.exception('Could not parse network: %s', net)
                continue

            OfficeNet.objects.create(
                office_id=office_data[0],
                net=net,
                name=office_data[1] or 'VPN',
                first_ip_value=first,
                last_ip_value=last,
            )


def bulk_insert_or_update(last_offices):
    not_uniq_fields = ('office_id', 'office_name', 'office_name_en', 'is_vpn', 'updated_at')
    fields = ('staff_id',) + not_uniq_fields
    sql = f'INSERT INTO {StaffLastOffice._meta.db_table} ({", ".join(fields)}) VALUES '
    args = []
    for office in last_offices:
        values = (
            office.staff_id,
            office.office_id,
            office.office_name,
            office.office_name_en,
            office.is_vpn,
            office.updated_at,
        )
        args += values
    rows_count = len(last_offices)
    fields_count = len(fields)
    percents_string = ', '.join(['%s'] * fields_count)
    sql += ', '.join([f'({percents_string})'] * rows_count)
    sql += ' ON CONFLICT (staff_id) DO UPDATE '
    sql += f'SET ({", ".join(not_uniq_fields)}) = (EXCLUDED.{", EXCLUDED.".join(not_uniq_fields)});'
    with connection.cursor() as cursor:
        cursor.execute(sql, args)


def fill_last_office(last_activities: Dict[int, Dict[str, Any]]):
    staff_last_offices = []
    for staff_id, last_activity in last_activities.items():
        staff_last_office = StaffLastOffice(
            staff_id=staff_id,
            updated_at=last_activity['updated_at'],
            office_id=last_activity['office_id'] or None,
            office_name=last_activity['name'],
            office_name_en=last_activity['name_en'],
            is_vpn=last_activity['is_vpn'],
        )
        staff_last_offices.append(staff_last_office)

    if staff_last_offices:
        bulk_insert_or_update(staff_last_offices)
    logger.info('Upserted %s last activities', len(staff_last_offices))


@app.task
class UpdateLastActivitiesPostgres(LockedTask):
    def locked_run(self, *args, **kwargs):
        logger.debug('UpdateLastActivities task started')

        activities = query()
        logger.info('Got %s raw activities from whistlah', len(activities))
        if not activities:
            return

        last_activities = parse_postgres_activity(activities)
        logger.info('Got %s activities after grouping by login', len(last_activities))
        round_parse_result_to_minute(last_activities)
        last_activities = replace_login_with_id(last_activities)
        last_activities = remove_already_existing_activities(last_activities)
        if not last_activities:
            logger.info('Nothing to update')
            return
        fill_last_office(last_activities)

        logger.debug('UpdateLastActivities task finished')
