# (c) cavaliba.com - sirene - notify.py

import json
import uuid
from datetime import datetime, timedelta

import html2text
from celery import shared_task
from django.utils import timezone

import app_home.cache as cache
from app_data.aaa import get_username
from app_data.data import Instance
from app_data.models import DataEAV
from app_data.group import Group
from app_data.user import User
from app_home.configuration import get_configuration
from app_home.log import DEBUG, WARNING, log
from app_sirene.mail import sirene_html_email, task_send_mail
from app_sirene.sms import get_sms_quota, task_send_sms


# ------------------------------------------------------
@shared_task(ignore_result=True)
def archive_expired_sirene(aaa=None):
    cache.init()
    count = archive_expired()
    log(
        DEBUG,
        aaa=aaa,
        app="sirene",
        view="task",
        action="archive",
        status="OK",
        data=f"{count} archived",
    )


# ------------------------------------------------------
def aaa_message_allowed(instance=None, aaa=None):
    """
    Check if instance() of sirene_message can be accessed by the aaa user.
    For detail/remove/archive/... views
    """

    if not instance.is_field_true("is_restricted"):
        return True

    if "p_sirene_access_restricted" in aaa["perms"]:
        return True

    try:
        username = aaa["username"]
    except Exception:
        return False

    try:
        if username in instance.fields["notified_username"].value:
            return True
    except Exception:
        pass
    return False


# ------------------------------------------------------
def is_24():
    """return True if current time outside business hours  Mo-Fr 08h00-18h00"""

    dt = timezone.now()
    wd = dt.weekday()  # Week =0-4 WEnd=5/6
    ho = dt.hour

    if wd > 4:
        return True

    if ho < 8:
        return True

    if ho > 17:
        return True

    return False


# ------------------------------------------------------
def archive_message(id=None, aaa=None):

    message = Instance.from_id(id, expand=True)
    if not message or message.classname != "sirene_message":
        return "no message"

    if aaa:
        username = aaa.get("username", "")
    else:
        username = "auto"

    message.set_field_value_single(
        "removed_at", str(datetime.today().strftime("%Y-%m-%d %H:%M:%S"))
    )
    message.set_field_value_single("removed_by", username)
    message.disable()

    return


# ------------------------------------------------------
def archive_all(aaa=None):

    count = 0

    instances = get_message_enabled()
    if not instances:
        return count

    for instance in instances:
        err = archive_message(id=instance.id, aaa=aaa)
        if not err:
            count += 1

    return count


# ------------------------------------------------------
def archive_expired(aaa=None):

    count = 0
    default_duration = int(get_configuration("sirene", "DEFAULT_DURATION_MINUTE"))

    instances = get_message_enabled()
    if not instances:
        return count

    for instance in instances:
        datestr1 = instance.get_attribute_first("created_at")
        datestr2 = instance.get_attribute_first("updated_at")
        if datestr2:
            datestr = datestr2
        else:
            datestr = datestr1
        try:
            date = datetime.strptime(datestr, "%Y-%m-%d %H:%M:%S")
        except Exception:
            continue
        date_expire = date + timedelta(minutes=default_duration)
        date_now = datetime.now()
        if date_expire <= date_now:
            # to archive
            err = archive_message(id=instance.id)
            if not err:
                count += 1
                log(
                    DEBUG,
                    aaa=aaa,
                    app="sirene",
                    view="archive",
                    action="expired",
                    status="OK",
                    data=f"{instance.id}",
                )
            else:
                log(
                    WARNING,
                    aaa=aaa,
                    app="sirene",
                    view="archive",
                    action="expired",
                    status="KO",
                    data=f"{instance.id}",
                )

    return count


# ------------------------------------------------------
def reopen_message(instance=None):

    if not instance or instance.classname != "sirene_message":
        return False

    return instance.enable()


def get_message_enabled():
    """OUT: list[] of sirene_message Instance()"""
    return Instance.iterate_classname(classname="sirene_message", enabled="yes", expand=True)


# ------------------------------------------------------
def get_public_active():
    """
    OUT: list[] of sirene_public Instance() for each sirene_message
    is_enabled message only
    is_enabled public page only
    """

    reply = []

    for instance in Instance.iterate_classname(classname="sirene_message", enabled="yes"):
        # exclude private (restricted) message from anonymous
        if instance.is_field_true("is_restricted"):
            continue

        # get public page
        public_name = instance.fields["public_page"].get_first_value()
        instance_public = Instance.from_keyname(classname="sirene_public", keyname=public_name)

        # exclude messages with no public page
        if not instance_public:
            continue
        if not instance_public.keyname:
            continue
        if not instance_public.is_enabled:
            continue

        # skip "default" (ok) message ;displayed only for an empty screen
        if instance_public.is_field_true("is_default"):
            continue

        reply.append(instance_public)

    return reply


# ------------------------------------------------------
def get_public_default():
    # OUT: [Instance()]

    for instance in Instance.iterate_classname(classname="sirene_public", enabled="yes"):
        if instance.is_field_true("is_default"):
            return [instance]
    return []


def textify(html):
    text = html2text.html2text(html)
    return text


# ------------------------------------------------------
def get_updates(instance=None):

    if not instance:
        return

    try:
        v = json.loads(instance.fields["update"].value[0])
        if type(v) is not list:
            v = []
    except Exception:
        v = []

    return v


# ------------------------------------------------------
def set_updates(instance=None, updates=None):

    if not instance:
        return False

    try:
        v = json.dumps(updates, ensure_ascii=False)
    except Exception:
        return False

    try:
        instance.fields["update"].value = [v]
    except Exception:
        return False

    return True


# ------------------------------------------------------
def count_userlist(instance=None, maxcount=0):
    """
    count people/email/sms from Instance notify_to fields / has_email or sms
    IN: sirene_message Instance()
    OUT: usercount, emailcount, smscount
    """

    userlist = expand(instance=instance, maxcount=maxcount)

    usercount = len(userlist)
    if instance.is_field_true("has_email"):
        emailcount = len(get_email_list(userlist))
    else:
        emailcount = 0
    if instance.is_field_true("has_sms"):
        smscount = len(get_sms_list(userlist))
    else:
        smscount = 0

    return usercount, emailcount, smscount


# ------------------------------------------------------
def get_email_list(userlist: list[User]) -> list[str]:
    """
    IN: list of User() objects
    OUT: list of unique email strings
    """

    email_list: set[str] = set()

    for user in userlist:
        if not user.want_notifications:
            continue

        if not user.want_24:
            if is_24():
                continue

        email = user.get_email()

        if email:
            email_list.add(email)

    return list(email_list)


# ------------------------------------------------------
def get_sms_list(userlist: list[User]) -> list[str]:

    sms_list: set[str] = set()

    for user in userlist:
        if not user.is_enabled:
            continue
        if not user.want_notifications:
            continue
        if not user.want_24:
            if is_24():
                continue

        mobile = user.get_mobile()
        if mobile:
            sms_list.add(mobile)

    return list(sms_list)


# ------------------------------------------------------
def set_placeholder_load(data, message, source_instance=None):
    """
    used at new message init before edit ; populate with Instance info
    replace $xxxx$ in string message, by info from source_instance
    """
    data2 = data

    # Instance field substitutions: $keyname$, $displayname$, $<fieldname>$ - V4.0
    if source_instance:
        data2 = data2.replace("$keyname$", source_instance.keyname or "")
        data2 = data2.replace("$displayname$", source_instance.displayname or "")
        for fieldname, field in source_instance.fields.items():
            placeholder = f"${fieldname}$"
            if placeholder in data2:
                try:
                    data2 = data2.replace(placeholder, str(field.value[0]))
                except Exception:
                    pass

    return data2


# ------------------------------------------------------
def set_placeholder_send(data, message, source_instance=None):
    """
    used before sending message after edit.
    Replace Severity / Category selected during edit
    """
    data2 = data

    if "$C$" in data2:
        try:
            data2 = data2.replace("$C$", message.fields["category"].value[0])
        except Exception:
            pass
    if "$S$" in data2:
        try:
            data2 = data2.replace("$S$", message.fields["severity"].value[0])
        except Exception:
            pass

    return data2


# -------------------------------------------------------------------------
def message_from_request(request=None, aaa=None):
    """
    Create a sirene_message Instance() from POST request
    Apply placeholder to displayname/content (no prefix here, see notify)
    Save to DB fi valid
    OUT: sirene_message or None
    """
    if not request:
        return None

    message = Instance(classname="sirene_message")
    message.merge_request(request, aaa=aaa)
    message.keyname = str(uuid.uuid4())
    message.is_enabled = True
    # TODO: store template name

    # no public page if is_restricted
    try:
        if message.is_field_true("is_restricted"):
            message.fields["public_page"].value = []
    except Exception:
        pass

    message.fields["created_at"].value = [str(datetime.today().strftime("%Y-%m-%d %H:%M:%S"))]

    # placeholder : $C$ = category : $S$ = severity
    message.displayname = set_placeholder_send(message.displayname, message)
    content = message.get_attribute_first("content")
    message.fields["content"].value = [set_placeholder_send(content, message)]

    try:
        message.fields["created_by"].value = [get_username(aaa=aaa)]
    except Exception:
        message.fields["created_by"].value = "n/a"

    message.fields["created_at"].value = [str(datetime.today().strftime("%Y-%m-%d %H:%M:%S"))]

    if not message.is_valid():
        return

    message.create()
    return message


# ------------------------------------------------------
def message_notify(message=None, aaa=None):
    """
    notify a sirene_message:
    - call expand to compute targets
    - apply placeholder $c$, $s$
    - create tasks to send email/sms
    update sirene_message and save state / count to DB
    IN:
    - sirene_message Instance()
    - aaa struct
    OUT: True/False
    """

    if not message:
        return False

    # compute list of User()
    user_max = int(get_configuration("sirene", "USER_MAX_NOTIFICATION"))
    userlist = expand(instance=message, maxcount=user_max)

    message.fields["notified_username"].value = [u.keyname for u in userlist]
    message.update()

    # send email
    #  ----------
    email_list = get_email_list(userlist)
    email_count = len(email_list)

    if not message.is_field_true("has_email"):
        email_count = 0

    email_max = int(get_configuration("sirene", "EMAIL_MAX_NOTIFICATION"))
    if email_count > email_max:
        email_count = 0

    if email_count > 0:
        prefix = get_configuration("sirene", "EMAIL_PREFIX")
        prefix = set_placeholder_send(prefix, message)
        subject = prefix + " " + message.displayname

        html_content = sirene_html_email(message=message)
        text_content = textify(html_content)
        try:
            task_send_mail.delay(
                subject, text_content, email_list, html_content=html_content, aaa=aaa
            )
        except Exception as e:
            print("task exception: ", e)
            email_count = 0

    # send sms
    # --------
    sms_list = get_sms_list(userlist)
    sms_count = len(sms_list)

    if not message.is_field_true("has_sms"):
        sms_count = 0

    sms_max = int(get_configuration("sirene", "SMS_MAX_NOTIFICATION"))
    if sms_count > sms_max:
        sms_count = 0

    smsbody = message.get_attribute_first("sms_content")
    if len(smsbody) == 0:
        sms_count = 0

    if sms_count > 0:
        sms_left = get_sms_quota(aaa)
        if sms_left - sms_count >= 0:
            prefix = get_configuration("sirene", "SMS_PREFIX")
            prefix = set_placeholder_send(prefix, message)
            # text_content = prefix + ' ' + nm.displayname
            # body = message.get_attribute_first('sms_content')
            # body = textify(body)
            text_content = prefix + " " + smsbody
            try:
                task_send_sms.delay(sms_list, text_content, aaa=aaa)
            except Exception as e:
                print("task exception: ", e)
                sms_count = 0

    message.set_field_value_single("email_count", email_count)
    message.set_field_value_single("sms_count", sms_count)
    message.update()

    return True


# -------------------------------------------------------------------------
def message_update(instance=None, aaa=None, mu=None):
    """
    update sirene_message Instance() in DB with mu message update structure
    call tasks to send email/sms
    IN:
    - instance is a sirene_message
    - mu is a dict (has_email, has_sms, content, sms_content, created_at, created_by)
    OUT: err, email_count, sms_count
    """
    if not isinstance(instance, Instance) and instance.classname == "sirene_message":
        # if not instance:
        return "no instance", 0, 0

    if not mu:
        return "no update", 0, 0

    # from view/form: content, has_email, has_sms
    try:
        mu["created_by"] = get_username(aaa=aaa)
    except Exception:
        mu["created_by"] = "n/a"

    mu["created_at"] = str(datetime.today().strftime("%Y-%m-%d %H:%M:%S"))

    instance.fields["updated_at"].value = [mu["created_at"]]
    instance.fields["updated_by"].value = [mu["created_by"]]

    # Notify update
    user_max = int(get_configuration("sirene", "USER_MAX_NOTIFICATION"))
    userlist = expand(instance=instance, maxcount=user_max)

    # placeholder $C$ $S$ in update content
    mu["content"] = set_placeholder_send(mu["content"], instance)

    # email
    #  -----
    email_list = get_email_list(userlist)
    email_count = len(email_list)

    if not mu["has_email"]:
        email_count = 0

    email_max = int(get_configuration("sirene", "EMAIL_MAX_NOTIFICATION"))
    if email_count > email_max:
        email_count = 0

    if email_count > 0:
        title = instance.displayname

        prefix = get_configuration("sirene", "EMAIL_UPDATE_PREFIX")
        prefix = set_placeholder_send(prefix, instance)
        subject = prefix + " " + title

        updates = get_updates(instance=instance)
        updates.append(mu)
        html_content = sirene_html_email(message=instance, updates=updates)
        text_content = textify(html_content)

        task_send_mail.delay(subject, text_content, email_list, html_content=html_content, aaa=aaa)

    # sms
    # ---
    sms_list = get_sms_list(userlist)
    sms_count = len(sms_list)

    if not mu["has_sms"]:
        sms_count = 0

    smsbody = mu["sms_content"]
    if len(smsbody) == 0:
        sms_count = 0

    sms_max = int(get_configuration("sirene", "SMS_MAX_NOTIFICATION"))
    if sms_count > sms_max:
        sms_count = 0

    if sms_count > 0:
        sms_left = get_sms_quota(aaa)
        if sms_left - sms_count >= 0:
            prefix = get_configuration("sirene", "SMS_UPDATE_PREFIX")
            prefix = set_placeholder_send(prefix, instance)
            # body = mu['sms_content']
            # body = textify(body)
            text_content = prefix + " " + smsbody
            task_send_sms.delay(sms_list, text_content, aaa=aaa)

    # save update
    # -----------
    mu["email_count"] = email_count
    mu["sms_count"] = sms_count
    # total_email = instance.fields['email_count'].value[0] + email_count
    # total_sms   = instance.fields['sms_count'].value[0] + sms_count
    # instance.set_field_value_single('email_count', total_email)
    # instance.set_field_value_single('sms_count', total_sms)
    v = get_updates(instance=instance)
    v.append(mu)
    r = set_updates(instance, v)
    instance.update()

    return None, email_count, sms_count


# ===========================================================================
# expand Instance sirene_message to userlist of db User objects to notify
# ===========================================================================
def init_watchlist():
    """get notify subscriptions from conf"""

    reply = {}
    # site:app  x:y z:t ...
    notify_subscriptions = get_configuration("sirene", "NOTIFY_SUBSCRIPTIONS")
    subscriptions = notify_subscriptions.split()
    for subscription in subscriptions:
        if ":" in subscription:
            try:
                (subscriber_schema, subscribed_schema) = subscription.split(":")
                reply[subscribed_schema] = []
            except Exception:
                pass
    return reply


def expand(instance=None, maxcount=0):
    """
    compute list of targeted users limited to maxcount.

    IN:
    - instance is a sirene_message Instance() with notify_xxxx fields
    - maxcount max users expanded/returned :

    OUT: [User(), User(), ...]

    is_enabled users only ; not filtered by user prefs
    """

    allusers = []
    allusers_keyname: set[str] = set()
    allgroups = []
    allgroups_keyname: set[str] = set()

    if not instance:
        return []

    tmp = get_configuration("sirene", "NOTIFY_FIELDS")
    try:
        notify_fields = tmp.split()
    except Exception:
        notify_fields = ["user", "group"]

    # dict of empty list : subscribed schemas (site:app >>> gives app)
    watchlist = init_watchlist()

    for targetfield in notify_fields:
        # direct
        # ------
        if targetfield == "user":
            if "notify_user" in instance.fields:
                for username in instance.fields["notify_user"].value:
                    if username not in allusers_keyname:
                        user = User.from_keyname(keyname=username)
                        if user and user.is_enabled:
                            allusers.append(user)
                            allusers_keyname.add(username)

        #  targetfield == notify_group => groups (expand at the end)
        elif targetfield == "group":
            if "notify_group" in instance.fields:
                for groupname in instance.fields["notify_group"].value:
                    if groupname not in allgroups_keyname:
                        group = Group.from_keyname(keyname=groupname)
                        if group and group.is_enabled:
                            allgroups.append(group)
                            allgroups_keyname.add(group.keyname)

        # targetfield is a schema other than user and group
        else:
            schema = targetfield
            fieldname = "notify_" + schema

            if fieldname not in instance.fields:
                continue

            # loop over each value in notify_xxxx field
            for iname in instance.fields[fieldname].value:
                subinstance = Instance.from_keyname(classname=schema, keyname=iname)

                if not subinstance:
                    continue

                # update watchlist for subscription
                if schema in watchlist:
                    watchlist[schema].append(iname)

                # nested user
                if "notify_user" in subinstance.fields:
                    for username in subinstance.fields["notify_user"].value:
                        if username not in allusers_keyname:
                            user = User.from_keyname(keyname=username)
                            if user and user.is_enabled:
                                allusers.append(user)
                                allusers_keyname.add(username)

                #  nested group
                if "notify_group" in subinstance.fields:
                    for groupname in subinstance.fields["notify_group"].value:
                        if groupname not in allgroups_keyname:
                            group = Group.from_keyname(keyname=groupname)
                            if group and group.is_enabled:
                                allgroups.append(group)
                                allgroups_keyname.add(group.keyname)

    # indirect (subscriptions)
    # subscription == site:app  => site subscribed to app(s) through notify_app field
    subscriptions_conf = get_configuration("sirene", "NOTIFY_SUBSCRIPTIONS")
    subscriptions = subscriptions_conf.split()

    for subscription in subscriptions:
        if ":" in subscription:
            try:
                (subscriber_schema, subscribed_schema) = subscription.split(":")
            except Exception:
                continue

            subscription_field = "notify_" + subscribed_schema

            if subscribed_schema not in watchlist:
                continue
            if len(watchlist[subscribed_schema]) == 0:
                continue

            # get all subscribed instance from all subscribers  : {} keyname => [field values]
            subscribers = {}
            for kn, val in DataEAV.objects.filter(
                classname=subscriber_schema, fieldname=subscription_field, is_enabled=True
            ).values_list("keyname", "value"):
                subscribers.setdefault(kn, []).append(val)

            # instances : site1 : app1
            for subscriber, subscribed in subscribers.items():
                #  intersect subscribed lists and watchlist
                found = False
                for i in subscribed:
                    if i in watchlist[subscribed_schema]:
                        found = True
                        break

                # this subscriber has not subscribed to an instance encountered (watchlist)
                if not found:
                    continue

                #  add this subscriber's user/group !

                # Get INSTANCE
                subinstance = Instance.from_keyname(classname=subscriber_schema, keyname=subscriber)
                if not subinstance:
                    continue

                # nested user
                if "notify_user" in subinstance.fields:
                    for username in subinstance.fields["notify_user"].value:
                        if username not in allusers_keyname:
                            user = User.from_keyname(keyname=username)
                            if user and user.is_enabled:
                                allusers.append(user)
                                allusers_keyname.add(username)

                #  nested group
                if "notify_group" in subinstance.fields:
                    for groupname in subinstance.fields["notify_group"].value:
                        if groupname not in allgroups_keyname:
                            group = Group.from_keyname(keyname=groupname)
                            if group and group.is_enabled:
                                allgroups.append(group)
                                allgroups_keyname.add(group.keyname)

    # Retrive groups, merge and Limit MAX
    for group in allgroups:
        for user in group.get_users_all():
            if user and user.is_enabled and user.keyname not in allusers_keyname:
                allusers.append(user)
                allusers_keyname.add(user.keyname)
                if maxcount > 0 and len(allusers_keyname) > maxcount:
                    return allusers

    return allusers


# ------------------------------------------------------
def send_test_sms(user=None, aaa=None):
    """
    Send a test SMS to user.get_mobile().
    Returns False if quota insufficient or number invalid.
    """
    from app_sirene.sms import get_sms_quota, sms_check_valid_number, task_send_sms

    if get_sms_quota(aaa) < 1:
        return False

    dest = user.get_mobile()
    if not sms_check_valid_number(dest):
        return False

    data = get_configuration("sirene", "SMS_TEST")
    task_send_sms.delay([dest], data, aaa=aaa)
    return True


# ------------------------------------------------------
def send_test_email(user=None, aaa=None):
    """
    Send a test email to user.get_email().
    Builds an HTML body using the configured mail template.
    """
    import os

    from django.template.loader import render_to_string

    subject = get_configuration("sirene", "EMAIL_TEST_SUBJECT")
    text_content = get_configuration("sirene", "EMAIL_TEST_CONTENT")
    dest = user.get_email()

    template = get_configuration("sirene", "EMAIL_MESSAGE_TEMPLATE")
    template_path = os.path.join("mail", template, "index.html")
    context = {
        "title": subject,
        "content": text_content,
        "category": "",
        "severity": "",
        "created_by": "",
        "created_at": "",
        "notify_target": user.keyname,
    }
    try:
        html_content = render_to_string(template_path, context)
    except Exception:
        html_content = None

    task_send_mail.delay(subject, text_content, [dest], html_content=html_content, aaa=aaa)
