import os
import uuid
from datetime import timedelta, datetime
from importlib import import_module

from django.conf import settings
from django.core.cache import caches
from django.db import models
from django.db.models import Q, CharField, F, Value
from django.db.models.functions import Cast, Coalesce, Concat
from django.utils import timezone
from django.utils.translation import gettext, gettext_lazy as _

from common.db.encoder import ModelJSONFieldEncoder
from common.sessions.cache import user_session_manager
from common.utils import lazyproperty, i18n_trans, get_ip_city
from ops.models import JobExecution
from orgs.mixins.models import OrgModelMixin, Organization
from orgs.utils import current_org
from terminal.models import default_storage
from .const import (
    OperateChoices,
    ActionChoices,
    ActivityChoices,
    LoginTypeChoices,
    MFAChoices,
    LoginStatusChoices,
)

__all__ = [
    "JobLog",
    "FTPLog",
    "OperateLog",
    "UserSession",
    "ActivityLog",
    "UserLoginLog",
    "PasswordChangeLog",
    "IntegrationApplicationLog",
]


def _get_city_display(ip, city='') -> str:
    try:
        return get_ip_city(ip) or gettext(city or '')
    except Exception:
        return gettext(city or '')


class JobLog(JobExecution):
    @property
    def creator_name(self) -> str:
        return self.creator.name

    class Meta:
        proxy = True
        verbose_name = _("Job audit log")


class FTPLog(OrgModelMixin):
    upload_to = 'FTP_FILES'

    id = models.UUIDField(default=uuid.uuid4, primary_key=True)
    user = models.CharField(max_length=128, verbose_name=_("User"))
    remote_addr = models.CharField(
        max_length=128, verbose_name=_("Remote addr"), blank=True, null=True
    )
    asset = models.CharField(max_length=768, verbose_name=_("Asset"), db_index=True)
    account = models.CharField(max_length=128, verbose_name=_("Account"), db_index=True)
    operate = models.CharField(
        max_length=16, verbose_name=_("Operate"), choices=OperateChoices.choices
    )
    filename = models.CharField(max_length=1024, verbose_name=_("Filename"))
    is_success = models.BooleanField(default=True, verbose_name=_("Success"))
    date_start = models.DateTimeField(auto_now_add=True, verbose_name=_("Date start"))
    has_file = models.BooleanField(default=False, verbose_name=_("Can Download"))
    session = models.CharField(max_length=36, verbose_name=_("Session"), default=uuid.uuid4)

    class Meta:
        verbose_name = _("File transfer log")
        indexes = [
            models.Index(fields=['date_start', 'org_id'], name='idx_date_start_org'),
        ]

    def __str__(self):
        return "{0.id} of {0.user} to {0.asset}".format(self)

    @property
    def filepath(self):
        return os.path.join(self.upload_to, self.date_start.strftime('%Y-%m-%d'), str(self.id))

    def save_file_to_storage(self, file):
        try:
            name = default_storage.save(self.filepath, file)
        except OSError as e:
            return None, e

        if settings.SERVER_REPLAY_STORAGE:
            from .tasks import upload_ftp_file_to_external_storage
            upload_ftp_file_to_external_storage.delay(str(self.id), file.name)
        return name, None


class OperateLog(OrgModelMixin):
    id = models.UUIDField(default=uuid.uuid4, primary_key=True)
    user = models.CharField(max_length=128, verbose_name=_("User"))
    action = models.CharField(
        max_length=16, choices=ActionChoices.choices, verbose_name=_("Action")
    )
    resource_type = models.CharField(max_length=64, verbose_name=_("Resource Type"))
    resource = models.CharField(max_length=128, verbose_name=_("Resource"))
    resource_id = models.CharField(
        max_length=128, blank=True, default='', db_index=True,
        verbose_name=_("Resource")
    )
    remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True)
    datetime = models.DateTimeField(auto_now=True, verbose_name=_('Datetime'), db_index=True)
    diff = models.JSONField(default=dict, encoder=ModelJSONFieldEncoder, null=True)

    def __str__(self):
        return "<{}> {} <{}>".format(self.user, self.action, self.resource)

    @lazyproperty
    def resource_type_display(self):
        return gettext(self.resource_type)

    def save(self, *args, **kwargs):
        if current_org.is_root() and not self.org_id:
            self.org_id = Organization.ROOT_ID
        return super(OperateLog, self).save(*args, **kwargs)

    @classmethod
    def from_dict(cls, d):
        self = cls()
        for k, v in d.items():
            setattr(self, k, v)
        return self

    @classmethod
    def from_multi_dict(cls, l):
        operate_logs = []
        for d in l:
            operate_log = cls.from_dict(d)
            operate_logs.append(operate_log)
        return operate_logs

    class Meta:
        verbose_name = _("Operate log")
        ordering = ('-datetime',)


class ActivityLog(OrgModelMixin):
    id = models.UUIDField(default=uuid.uuid4, primary_key=True)
    type = models.CharField(
        choices=ActivityChoices.choices, max_length=2,
        null=True, default=None, verbose_name=_("Activity type"),
    )
    resource_id = models.CharField(
        max_length=36, blank=True, default='',
        db_index=True, verbose_name=_("Resource")
    )
    datetime = models.DateTimeField(
        auto_now=True, verbose_name=_('Datetime'), db_index=True
    )
    # 日志的描述信息
    detail = models.TextField(default='', blank=True, verbose_name=_('Detail'))
    # 详情ID, 结合 type 来使用, (实例ID 和 CeleryTaskID)
    detail_id = models.CharField(
        max_length=36, default=None, null=True, verbose_name=_('Detail ID')
    )

    class Meta:
        verbose_name = _("Activity log")
        ordering = ('-datetime',)

    def __str__(self):
        detail = i18n_trans(self.detail)
        return "{} {}".format(detail, self.resource_id)

    def save(self, *args, **kwargs):
        if current_org.is_root() and not self.org_id:
            self.org_id = Organization.ROOT_ID
        return super(ActivityLog, self).save(*args, **kwargs)


def user_display_expression():
    return Concat(
        Coalesce(Cast(F('name'), CharField()), Value('None')),
        Value('('),
        Coalesce(Cast(F('username'), CharField()), Value('None')),
        Value(')'),
        output_field=CharField(),
    )


class PasswordChangeLog(models.Model):
    id = models.UUIDField(default=uuid.uuid4, primary_key=True)
    user = models.CharField(max_length=128, verbose_name=_("User"))
    change_by = models.CharField(max_length=128, verbose_name=_("Change by"))
    remote_addr = models.CharField(
        max_length=128, verbose_name=_("Remote addr"), blank=True, null=True
    )
    datetime = models.DateTimeField(auto_now=True, verbose_name=_("Datetime"), db_index=True)

    def __str__(self):
        return "{} change {}'s password".format(self.change_by, self.user)

    class Meta:
        verbose_name = _("Password change log")

    @staticmethod
    def filter_queryset_by_org(queryset):
        if not current_org.is_root():
            users = (
                current_org.get_members()
                .annotate(user_display=user_display_expression())
                .values_list('user_display', flat=True)
            )
            queryset = queryset.filter(
                user__in=[str(user) for user in users]
            )
        return queryset


class UserLoginLog(models.Model):
    id = models.UUIDField(default=uuid.uuid4, primary_key=True)
    username = models.CharField(max_length=128, verbose_name=_("Username"))
    type = models.CharField(
        choices=LoginTypeChoices.choices, max_length=2, verbose_name=_("Login type")
    )
    ip = models.GenericIPAddressField(verbose_name=_("Login IP"))
    city = models.CharField(
        max_length=254, blank=True, null=True, verbose_name=_("Login city")
    )
    user_agent = models.CharField(
        max_length=254, blank=True, null=True, verbose_name=_("User agent")
    )
    mfa = models.SmallIntegerField(
        default=MFAChoices.unknown, choices=MFAChoices.choices, verbose_name=_("MFA")
    )
    reason = models.CharField(
        default="", max_length=128, blank=True, verbose_name=_("Reason")
    )
    reason_code = models.CharField(
        default='', max_length=64, blank=True, verbose_name=_("Reason code")
    )
    reason_params = models.JSONField(
        default=dict, blank=True, verbose_name=_("Reason params")
    )
    status = models.BooleanField(
        default=LoginStatusChoices.success,
        choices=LoginStatusChoices.choices,
        verbose_name=_("Status"),
    )
    datetime = models.DateTimeField(default=timezone.now, verbose_name=_("Login Date"), db_index=True)
    backend = models.CharField(
        max_length=32, default="", verbose_name=_("Auth backend")
    )

    def __str__(self):
        return '%s(%s)' % (self.username, self.city)

    @property
    def backend_display(self) -> str:
        return gettext(self.backend)

    @lazyproperty
    def city_display(self) -> str:
        return _get_city_display(self.ip, self.city)

    @classmethod
    def get_login_logs(cls, date_from=None, date_to=None, user=None, keyword=None):
        login_logs = cls.objects.all()
        if date_from and date_to:
            date_from = "{} {}".format(date_from, "00:00:00")
            date_to = "{} {}".format(date_to, "23:59:59")
            login_logs = login_logs.filter(
                datetime__gte=date_from, datetime__lte=date_to
            )
        if user:
            login_logs = login_logs.filter(username=user)
        if keyword:
            login_logs = login_logs.filter(
                Q(ip__contains=keyword)
                | Q(city__contains=keyword)
                | Q(username__contains=keyword)
            )
        if not current_org.is_root():
            username_list = current_org.get_members().values_list("username", flat=True)
            login_logs = login_logs.filter(username__in=username_list)
        return login_logs

    @property
    def reason_display(self) -> str:
        from authentication.errors import (
            reason_choices, reason_template_choices, old_reason_choices
        )

        template = reason_template_choices.get(self.reason_code)
        if template:
            reason_params = self.reason_params
            if not isinstance(reason_params, dict):
                reason_params = {}
            try:
                return template.format(**reason_params)
            except (AttributeError, KeyError, IndexError, TypeError, ValueError):
                pass

        reason = reason_choices.get(self.reason_code)
        if reason:
            return reason

        reason = reason_choices.get(self.reason)
        if reason:
            return reason
        reason = old_reason_choices.get(self.reason, self.reason)
        return reason

    @staticmethod
    def filter_queryset_by_org(queryset):
        from audits.utils import construct_userlogin_usernames
        if current_org.is_root() or not settings.XPACK_ENABLED:
            return queryset
        user_queryset = current_org.get_members()
        users = construct_userlogin_usernames(user_queryset)
        return queryset.filter(username__in=users)

    class Meta:
        ordering = ["-datetime", "username"]
        verbose_name = _("User login log")


class UserSession(models.Model):
    _OPERATE_LOG_ACTION = {'delete': ActionChoices.finished}

    id = models.UUIDField(default=uuid.uuid4, primary_key=True)
    ip = models.GenericIPAddressField(verbose_name=_("Login IP"))
    key = models.CharField(max_length=128, verbose_name=_("Session key"))
    city = models.CharField(max_length=254, blank=True, null=True, verbose_name=_("Login city"))
    user_agent = models.CharField(max_length=254, blank=True, null=True, verbose_name=_("User agent"))
    type = models.CharField(choices=LoginTypeChoices.choices, max_length=2, verbose_name=_("Login type"))
    backend = models.CharField(max_length=32, default="", verbose_name=_("Auth backend"))
    date_created = models.DateTimeField(null=True, blank=True, verbose_name=_('Login date'))
    user = models.ForeignKey(
        'users.User', verbose_name=_('User'), related_name='sessions', on_delete=models.CASCADE
    )

    def __str__(self):
        return '%s(%s)' % (self.user, self.ip)

    @lazyproperty
    def city_display(self) -> str:
        return _get_city_display(self.ip, self.city)

    @property
    def backend_display(self) -> str:
        return gettext(self.backend)

    @property
    def is_active(self) -> bool:
        return user_session_manager.check_active(self.key)

    @property
    def date_expired(self) -> datetime:
        session_store_cls = import_module(settings.SESSION_ENGINE).SessionStore
        session_store = session_store_cls(session_key=self.key)
        cache_key = session_store.cache_key
        ttl = caches[settings.SESSION_CACHE_ALIAS].ttl(cache_key)
        return timezone.now() + timedelta(seconds=ttl)

    @classmethod
    def clear_expired_sessions(cls):
        keys = user_session_manager.get_keys()
        cls.objects.exclude(key__in=keys).delete()

    class Meta:
        ordering = ['-date_created']
        verbose_name = _('User session')
        permissions = [
            ('offline_usersession', _('Offline user session')),
        ]


class IntegrationApplicationLog(models.Model):
    id = models.UUIDField(default=uuid.uuid4, primary_key=True)
    remote_addr = models.GenericIPAddressField(verbose_name=_("Remote addr"))
    service = models.CharField(max_length=128, verbose_name=_("Application"))
    service_id = models.UUIDField(verbose_name=_("Application ID"))
    asset = models.CharField(max_length=128, verbose_name=_("Asset"))
    account = models.CharField(max_length=128, verbose_name=_("Account"))
    datetime = models.DateTimeField(auto_now=True, verbose_name=_("Datetime"))
