from collections import defaultdict

from django.core.cache import cache
from django.db.models import Count, Max, F, CharField, Q
from django.db.models.functions import Cast
from django.http.response import JsonResponse
from django.utils import timezone
from django.utils.timesince import timesince
from rest_framework.request import Request
from rest_framework.views import APIView

from assets.const import AllTypes
from assets.models import Asset
from audits.api import OperateLogViewSet
from audits.const import LoginStatusChoices
from audits.models import UserLoginLog, PasswordChangeLog, OperateLog, FTPLog, JobLog
from audits.utils import construct_userlogin_usernames
from common.utils import lazyproperty
from common.utils.timezone import local_now, local_zero_hour
from ops.const import JobStatus
from orgs.caches import OrgResourceStatisticsCache
from orgs.utils import current_org, filter_org_queryset
from terminal.const import RiskLevelChoices, CommandStorageType
from terminal.models import Session, CommandStorage

__all__ = ['IndexApi']


class DateTimeMixin:
    request: Request

    @property
    def org(self):
        return current_org

    @lazyproperty
    def days(self):
        query_params = self.request.query_params
        count = query_params.get('days')
        count = int(count) if count else 1
        return count

    @property
    def days_to_datetime(self):
        days = self.days
        if days == 1:
            t = local_zero_hour()
        else:
            t = local_zero_hour() - timezone.timedelta(days=days)
        return t

    @lazyproperty
    def date_start_end(self):
        return self.days_to_datetime, local_now()

    @lazyproperty
    def dates_list(self):
        return [
            (local_now() - timezone.timedelta(days=i)).date()
            for i in range(self.days - 1, -1, -1)
        ]

    def get_dates_metrics_date(self):
        return [d.strftime('%m-%d') for d in self.dates_list] or ['0']

    def get_logs_queryset_filter(self, qs, query_field, is_timestamp=False):
        dt = self.days_to_datetime
        t = dt.timestamp() if is_timestamp else dt
        query = {f'{query_field}__gte': t}
        return qs.filter(**query)

    @lazyproperty
    def users(self):
        return self.org.get_members()

    def get_logs_queryset(self, queryset, query_params):
        query = {}
        users = self.users
        if not self.org.is_root():
            if query_params == 'username':
                query = {
                    f'{query_params}__in': construct_userlogin_usernames(users)
                }
            else:
                query = {
                    f'{query_params}__in': [str(user) for user in users]
                }
        queryset = queryset.filter(**query)
        return queryset

    @lazyproperty
    def sessions_queryset(self):
        return self.get_logs_queryset_filter(Session.objects, 'date_start')

    @lazyproperty
    def login_logs_queryset(self):
        qs = UserLoginLog.objects.all()
        qs = self.get_logs_queryset_filter(qs, 'datetime')
        queryset = self.get_logs_queryset(qs, 'username')
        return queryset

    @lazyproperty
    def user_login_logs_on_the_system_queryset(self):
        qs = UserLoginLog.objects.filter(status=LoginStatusChoices.success)
        qs = self.get_logs_queryset_filter(qs, 'datetime')
        queryset = qs.filter(username__in=construct_userlogin_usernames(self.users))
        return queryset

    @lazyproperty
    def password_change_logs_queryset(self):
        qs = PasswordChangeLog.objects.all()
        qs = self.get_logs_queryset_filter(qs, 'datetime')
        queryset = self.get_logs_queryset(qs, 'user')
        return queryset

    @lazyproperty
    def operate_logs_queryset(self):
        qs = OperateLogViewSet().get_queryset()
        return self.get_logs_queryset_filter(qs, 'datetime')

    @lazyproperty
    def ftp_logs_queryset(self):
        qs = FTPLog.objects.all()
        return self.get_logs_queryset_filter(qs, 'date_start')

    @lazyproperty
    def command_type_queryset_list(self):
        qs_list = []
        for storage in CommandStorage.objects.exclude(name='null'):
            if not storage.is_valid():
                continue

            qs = storage.get_command_queryset()
            qs = filter_org_queryset(qs)
            qs = self.get_logs_queryset_filter(
                qs, 'timestamp', is_timestamp=True
            )
            qs_list.append((storage.type, qs))
        return qs_list

    @lazyproperty
    def job_logs_queryset(self):
        qs = JobLog.objects.all()
        return self.get_logs_queryset_filter(qs, 'date_start')


class DatesLoginMetricMixin:
    days: int
    dates_list: list
    date_start_end: tuple
    command_type_queryset_list: list
    sessions_queryset: Session.objects
    ftp_logs_queryset: FTPLog.objects
    job_logs_queryset: JobLog.objects
    login_logs_queryset: UserLoginLog.objects
    user_login_logs_on_the_system_queryset: UserLoginLog.objects
    operate_logs_queryset: OperateLog.objects
    password_change_logs_queryset: PasswordChangeLog.objects

    CACHE_TIMEOUT = 60

    @lazyproperty
    def get_type_to_assets(self):
        result = Asset.objects.annotate(type=F('platform__type')). \
            values('type').order_by('type').annotate(total=Count(1))
        all_types_dict = dict(AllTypes.choices())
        result = list(result)
        for i in result:
            tp = i['type']
            i['label'] = all_types_dict.get(tp, tp)
        return result

    def filter_date_start_end(self, queryset, field_name):
        query = {f'{field_name}__range': self.date_start_end}
        return queryset.filter(**query)

    def get_date_metrics(self, queryset, field_name, count_fields):
        queryset = self.filter_date_start_end(queryset, field_name)

        if not isinstance(count_fields, (list, tuple)):
            count_fields = [count_fields]

        values_list = [field_name] + list(count_fields)
        queryset = queryset.values_list(*values_list)

        date_group_map = defaultdict(lambda: defaultdict(set))

        for row in queryset:
            datetime = row[0]
            date_str = str(datetime.date())
            for idx, count_field in enumerate(count_fields):
                date_group_map[date_str][count_field].add(row[idx + 1])

        date_metrics_dict = defaultdict(list)
        for field in count_fields:
            for date_str in self.dates_list:
                count = len(date_group_map.get(str(date_str), {}).get(field, set()))
                date_metrics_dict[field].append(count)

        return date_metrics_dict

    def get_dates_metrics_total_count_active_users_and_assets(self):
        date_metrics_dict = self.get_date_metrics(
            Session.objects, 'date_start', ('user_id', 'asset_id')
        )
        return date_metrics_dict.get('user_id', []), date_metrics_dict.get('asset_id', [])

    def get_dates_metrics_total_count_login(self):
        date_metrics_dict = self.get_date_metrics(
            UserLoginLog.objects, 'datetime', 'id'
        )
        return date_metrics_dict.get('id', [])

    def get_dates_metrics_total_count_sessions(self):
        date_metrics_dict = self.get_date_metrics(
            Session.objects, 'date_start', 'id'
        )
        return date_metrics_dict.get('id', [])

    def get_dates_login_times_assets(self):
        cache_key = f"stats:top10_assets:{self.days}"
        data = cache.get(cache_key)
        if data is not None:
            return data

        assets = self.sessions_queryset.values("asset") \
            .annotate(total=Count("asset")) \
            .annotate(last=Cast(Max("date_start"), output_field=CharField())) \
            .order_by("-total")

        result = list(assets[:10])
        cache.set(cache_key, result, self.CACHE_TIMEOUT)
        return result

    def get_dates_login_times_users(self):
        cache_key = f"stats:top10_users:{self.days}"
        data = cache.get(cache_key)
        if data is not None:
            return data

        users = self.sessions_queryset.values("user_id") \
            .annotate(total=Count("user_id")) \
            .annotate(user=Max('user')) \
            .annotate(last=Cast(Max("date_start"), output_field=CharField())) \
            .order_by("-total")
        result = list(users[:10])
        cache.set(cache_key, result, self.CACHE_TIMEOUT)
        return result

    def get_dates_login_record_sessions(self):
        sessions = self.sessions_queryset.order_by('-date_start')
        sessions = [
            {
                'user': session.user,
                'asset': session.asset,
                'is_finished': session.is_finished,
                'date_start': str(session.date_start),
                'timesince': timesince(session.date_start)
            }
            for session in sessions[:10]
        ]
        return sessions

    @lazyproperty
    def user_login_logs_amount(self):
        return self.login_logs_queryset.count()

    @lazyproperty
    def user_login_success_logs_amount(self):
        return self.login_logs_queryset.filter(status=LoginStatusChoices.success).count()

    @lazyproperty
    def user_login_amount(self):
        return self.user_login_logs_on_the_system_queryset.values('username').distinct().count()

    @lazyproperty
    def operate_logs_amount(self):
        return self.operate_logs_queryset.count()

    @lazyproperty
    def change_password_logs_amount(self):
        return self.password_change_logs_queryset.count()

    @lazyproperty
    def command_statistics(self):
        def _count_pair(_tp, _qs):
            if _tp == CommandStorageType.es:
                total = _qs.count(limit_to_max_result_window=False)
                danger = _qs.filter(risk_level=RiskLevelChoices.reject) \
                    .count(limit_to_max_result_window=False)
                return total, danger

            agg = _qs.aggregate(
                total=Count('pk'),
                danger=Count('pk', filter=Q(risk_level=RiskLevelChoices.reject)),
            )
            return (agg['total'] or 0), (agg['danger'] or 0)

        total_amount = 0
        danger_amount = 0
        for tp, qs in self.command_type_queryset_list:
            t, d = _count_pair(tp, qs)
            total_amount += t
            danger_amount += d
        return total_amount, danger_amount

    @lazyproperty
    def commands_amount(self):
        total_amount, __ = self.command_statistics
        return total_amount

    @lazyproperty
    def commands_danger_amount(self):
        __, danger_amount = self.command_statistics
        return danger_amount

    @lazyproperty
    def job_logs_running_amount(self):
        return self.job_logs_queryset.filter(status=JobStatus.running).count()

    @lazyproperty
    def job_logs_failed_amount(self):
        return self.job_logs_queryset.filter(
            status__in=[JobStatus.failed, JobStatus.timeout]
        ).count()

    @lazyproperty
    def job_logs_amount(self):
        return self.job_logs_queryset.count()

    @lazyproperty
    def sessions_amount(self):
        return self.sessions_queryset.count()

    @lazyproperty
    def online_sessions_amount(self):
        return self.sessions_queryset.filter(is_finished=False).count()

    @lazyproperty
    def ftp_logs_amount(self):
        return self.ftp_logs_queryset.count()


class IndexApi(DateTimeMixin, DatesLoginMetricMixin, APIView):
    http_method_names = ['get']
    rbac_perms = {
        'GET': ['rbac.view_audit | rbac.view_console'],
    }

    def get(self, request, *args, **kwargs):
        data = {}

        query_params = self.request.query_params

        caches = OrgResourceStatisticsCache(self.org)

        _all = query_params.get('all')

        if _all or query_params.get('total_count') or query_params.get('total_count_users'):
            data.update({
                'total_count_users': caches.users_amount,
            })

        if _all or query_params.get('total_count') or query_params.get('total_count_assets'):
            data.update({
                'total_count_assets': caches.assets_amount,
            })

        if _all or query_params.get('total_count') or query_params.get('total_count_users_this_week'):
            data.update({
                'total_count_users_this_week': caches.new_users_amount_this_week,
            })

        if _all or query_params.get('total_count') or query_params.get('total_count_assets_this_week'):
            data.update({
                'total_count_assets_this_week': caches.new_assets_amount_this_week,
            })

        if _all or query_params.get('total_count') or query_params.get('total_count_login_users'):
            data.update({
                'total_count_login_users': self.user_login_amount
            })

        if _all or query_params.get('total_count') or query_params.get('total_count_today_active_assets'):
            data.update({
                'total_count_today_active_assets': caches.total_count_today_active_assets,
            })

        if _all or query_params.get('total_count') or query_params.get('total_count_online_users'):
            data.update({
                'total_count_online_users': caches.total_count_online_users,
            })

        if _all or query_params.get('total_count') or query_params.get('total_count_online_sessions'):
            data.update({
                'total_count_online_sessions': caches.total_count_online_sessions,
            })

        if _all or query_params.get('total_count') or query_params.get('total_count_today_failed_sessions'):
            data.update({
                'total_count_today_failed_sessions': caches.total_count_today_failed_sessions,
            })

        if _all or query_params.get('total_count') or query_params.get('total_count_user_login_logs'):
            data.update({
                'total_count_user_login_logs': self.user_login_logs_amount,
            })

        if _all or query_params.get('total_count') or query_params.get('total_count_user_login_success_logs'):
            data.update({
                'total_count_user_login_success_logs': self.user_login_success_logs_amount,
            })

        if _all or query_params.get('total_count') or query_params.get('total_count_operate_logs'):
            data.update({
                'total_count_operate_logs': self.operate_logs_amount,
            })

        if _all or query_params.get('total_count') or query_params.get('total_count_change_password_logs'):
            data.update({
                'total_count_change_password_logs': self.change_password_logs_amount,
            })

        if _all or query_params.get('total_count') or query_params.get('total_count_commands'):
            data.update({
                'total_count_commands': self.commands_amount,
            })

        if _all or query_params.get('total_count') or query_params.get('total_count_commands_danger'):
            data.update({
                'total_count_commands_danger': self.commands_danger_amount,
            })

        if _all or query_params.get('total_count') or query_params.get('total_count_history_sessions'):
            data.update({
                'total_count_history_sessions': self.sessions_amount - self.online_sessions_amount,
            })

        if _all or query_params.get('total_count') or query_params.get('total_count_ftp_logs'):
            data.update({
                'total_count_ftp_logs': self.ftp_logs_amount,
            })

        if _all or query_params.get('total_count') or query_params.get('total_count_job_logs'):
            data.update({
                'total_count_job_logs': self.job_logs_amount,
            })

        if _all or query_params.get('total_count') or query_params.get('total_count_job_logs_running'):
            data.update({
                'total_count_job_logs_running': self.job_logs_running_amount,
            })

        if _all or query_params.get('total_count') or query_params.get('total_count_job_logs_failed'):
            data.update({
                'total_count_job_logs_failed': self.job_logs_failed_amount,
            })

        if _all or query_params.get('total_count') or query_params.get('total_count_type_to_assets_amount'):
            data.update({
                'total_count_type_to_assets_amount': self.get_type_to_assets,
            })

        if _all or query_params.get('session_dates_metrics'):
            data.update({
                'dates_metrics_date': self.get_dates_metrics_date(),
                'dates_metrics_total_count_session': self.get_dates_metrics_total_count_sessions(),
            })

        if _all or query_params.get('dates_metrics'):
            user_data, asset_data = self.get_dates_metrics_total_count_active_users_and_assets()
            login_data = self.get_dates_metrics_total_count_login()
            data.update({
                'dates_metrics_date': self.get_dates_metrics_date(),
                'dates_metrics_total_count_login': login_data,
                'dates_metrics_total_count_active_users': user_data,
                'dates_metrics_total_count_active_assets': asset_data,
            })

        if _all or query_params.get('dates_login_times_top10_assets'):
            data.update({
                'dates_login_times_top10_assets': self.get_dates_login_times_assets(),
            })

        if _all or query_params.get('dates_login_times_top10_users'):
            data.update({
                'dates_login_times_top10_users': self.get_dates_login_times_users(),
            })

        if _all or query_params.get('dates_login_record_top10_sessions'):
            data.update({
                'dates_login_record_top10_sessions': self.get_dates_login_record_sessions()
            })

        return JsonResponse(data, status=200)
