import time
import base64
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
from Crypto.Hash import SHA3_384
from Crypto.Util.Padding import pad
import validators
import msgpack
import re
from collections import Counter
from typing import List, Union, Any
from loguru import logger
import asyncio

from packaging import version as pyver
from distutils.version import LooseVersion
from nats.aio.client import Client as NATS
from nats.aio.errors import ErrTimeout

from django.db import models
from django.conf import settings
from django.utils import timezone as djangotime
from alerts.models import AlertTemplate

from core.models import CoreSettings, TZ_CHOICES
from logs.models import BaseAuditModel

logger.configure(**settings.LOG_CONFIG)


class Agent(BaseAuditModel):
    version = models.CharField(default="0.1.0", max_length=255)
    salt_ver = models.CharField(default="1.0.3", max_length=255)
    operating_system = models.CharField(null=True, blank=True, max_length=255)
    plat = models.CharField(max_length=255, null=True, blank=True)
    plat_release = models.CharField(max_length=255, null=True, blank=True)
    hostname = models.CharField(max_length=255)
    salt_id = models.CharField(null=True, blank=True, max_length=255)
    local_ip = models.TextField(null=True, blank=True)  # deprecated
    agent_id = models.CharField(max_length=200)
    last_seen = models.DateTimeField(null=True, blank=True)
    services = models.JSONField(null=True, blank=True)
    public_ip = models.CharField(null=True, max_length=255)
    total_ram = models.IntegerField(null=True, blank=True)
    used_ram = models.IntegerField(null=True, blank=True)  # deprecated
    disks = models.JSONField(null=True, blank=True)
    boot_time = models.FloatField(null=True, blank=True)
    logged_in_username = models.CharField(null=True, blank=True, max_length=255)
    last_logged_in_user = models.CharField(null=True, blank=True, max_length=255)
    antivirus = models.CharField(default="n/a", max_length=255)  # deprecated
    monitoring_type = models.CharField(max_length=30)
    description = models.CharField(null=True, blank=True, max_length=255)
    mesh_node_id = models.CharField(null=True, blank=True, max_length=255)
    overdue_email_alert = models.BooleanField(default=False)
    overdue_text_alert = models.BooleanField(default=False)
    overdue_dashboard_alert = models.BooleanField(default=False)
    offline_time = models.PositiveIntegerField(default=4)
    overdue_time = models.PositiveIntegerField(default=30)
    check_interval = models.PositiveIntegerField(default=120)
    needs_reboot = models.BooleanField(default=False)
    choco_installed = models.BooleanField(default=False)
    wmi_detail = models.JSONField(null=True, blank=True)
    patches_last_installed = models.DateTimeField(null=True, blank=True)
    time_zone = models.CharField(
        max_length=255, choices=TZ_CHOICES, null=True, blank=True
    )
    maintenance_mode = models.BooleanField(default=False)
    site = models.ForeignKey(
        "clients.Site",
        related_name="agents",
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
    )
    policy = models.ForeignKey(
        "automation.Policy",
        related_name="agents",
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
    )

    def save(self, *args, **kwargs):

        # get old agent if exists
        old_agent = type(self).objects.get(pk=self.pk) if self.pk else None
        super(BaseAuditModel, self).save(*args, **kwargs)

        # check if new agent has been create
        # or check if policy have changed on agent
        # or if site has changed on agent and if so generate-policies
        if (
            not old_agent
            or old_agent
            and old_agent.policy != self.policy
            or old_agent.site != self.site
        ):
            self.generate_checks_from_policies()
            self.generate_tasks_from_policies()

    def __str__(self):
        return self.hostname

    @property
    def client(self):
        return self.site.client

    @property
    def has_nats(self):
        return pyver.parse(self.version) >= pyver.parse("1.1.0")

    @property
    def has_gotasks(self):
        return pyver.parse(self.version) >= pyver.parse("1.1.1")

    @property
    def timezone(self):
        # return the default timezone unless the timezone is explicity set per agent
        if self.time_zone is not None:
            return self.time_zone
        else:
            from core.models import CoreSettings

            return CoreSettings.objects.first().default_time_zone

    @property
    def arch(self):
        if self.operating_system is not None:
            if "64 bit" in self.operating_system or "64bit" in self.operating_system:
                return "64"
            elif "32 bit" in self.operating_system or "32bit" in self.operating_system:
                return "32"
        return None

    @property
    def winagent_dl(self):
        if self.arch == "64":
            return settings.DL_64
        elif self.arch == "32":
            return settings.DL_32
        return None

    @property
    def win_inno_exe(self):
        if self.arch == "64":
            return f"winagent-v{settings.LATEST_AGENT_VER}.exe"
        elif self.arch == "32":
            return f"winagent-v{settings.LATEST_AGENT_VER}-x86.exe"
        return None

    @property
    def status(self):
        offline = djangotime.now() - djangotime.timedelta(minutes=self.offline_time)
        overdue = djangotime.now() - djangotime.timedelta(minutes=self.overdue_time)

        if self.last_seen is not None:
            if (self.last_seen < offline) and (self.last_seen > overdue):
                return "offline"
            elif (self.last_seen < offline) and (self.last_seen < overdue):
                return "overdue"
            else:
                return "online"
        else:
            return "offline"

    @property
    def has_patches_pending(self):
        return self.winupdates.filter(action="approve").filter(installed=False).exists()

    @property
    def checks(self):
        total, passing, failing = 0, 0, 0

        if self.agentchecks.exists():
            for i in self.agentchecks.all():
                total += 1
                if i.status == "passing":
                    passing += 1
                elif i.status == "failing":
                    failing += 1

        ret = {
            "total": total,
            "passing": passing,
            "failing": failing,
            "has_failing_checks": failing > 0,
        }
        return ret

    @property
    def cpu_model(self):
        ret = []
        try:
            cpus = self.wmi_detail["cpu"]
            for cpu in cpus:
                ret.append([x["Name"] for x in cpu if "Name" in x][0])
            return ret
        except:
            return ["unknown cpu model"]

    @property
    def local_ips(self):
        ret = []
        try:
            ips = self.wmi_detail["network_config"]
        except:
            return "error getting local ips"

        for i in ips:
            try:
                addr = [x["IPAddress"] for x in i if "IPAddress" in x][0]
            except:
                continue

            if addr is None:
                continue

            for ip in addr:
                if validators.ipv4(ip):
                    ret.append(ip)

        if len(ret) == 1:
            return ret[0]
        else:
            return ", ".join(ret) if ret else "error getting local ips"

    @property
    def make_model(self):
        try:
            comp_sys = self.wmi_detail["comp_sys"][0]
            comp_sys_prod = self.wmi_detail["comp_sys_prod"][0]
            make = [x["Vendor"] for x in comp_sys_prod if "Vendor" in x][0]
            model = [x["Model"] for x in comp_sys if "Model" in x][0]

            if "to be filled" in model.lower():
                mobo = self.wmi_detail["base_board"][0]
                make = [x["Manufacturer"] for x in mobo if "Manufacturer" in x][0]
                model = [x["Product"] for x in mobo if "Product" in x][0]

            return f"{make} {model}"
        except:
            pass

        try:
            return [x["Version"] for x in comp_sys_prod if "Version" in x][0]
        except:
            pass

        return "unknown make/model"

    @property
    def physical_disks(self):
        try:
            disks = self.wmi_detail["disk"]
            ret = []
            for disk in disks:
                interface_type = [
                    x["InterfaceType"] for x in disk if "InterfaceType" in x
                ][0]

                if interface_type == "USB":
                    continue

                model = [x["Caption"] for x in disk if "Caption" in x][0]
                size = [x["Size"] for x in disk if "Size" in x][0]

                size_in_gb = round(int(size) / 1_073_741_824)
                ret.append(f"{model} {size_in_gb:,}GB {interface_type}")

            return ret
        except:
            return ["unknown disk"]

    def run_script(
        self,
        scriptpk: int,
        args: List[str] = [],
        timeout: int = 120,
        full: bool = False,
        wait: bool = False,
        run_on_any: bool = False,
    ) -> Any:

        from scripts.models import Script

        script = Script.objects.get(pk=scriptpk)
        data = {
            "func": "runscriptfull" if full else "runscript",
            "timeout": timeout,
            "script_args": args,
            "payload": {
                "code": script.code,
                "shell": script.shell,
            },
        }

        running_agent = self
        if run_on_any:
            nats_ping = {"func": "ping", "timeout": 1}

            # try on self first
            r = asyncio.run(self.nats_cmd(nats_ping))

            if r == "pong":
                running_agent = self
            else:
                online = [
                    agent
                    for agent in Agent.objects.only(
                        "pk", "last_seen", "overdue_time", "offline_time"
                    )
                    if agent.status == "online"
                ]

                for agent in online:
                    r = asyncio.run(agent.nats_cmd(nats_ping))
                    if r == "pong":
                        running_agent = agent
                        break

                if running_agent.pk == self.pk:
                    return "Unable to find an online agent"

        if wait:
            return asyncio.run(running_agent.nats_cmd(data, timeout=timeout, wait=True))
        else:
            asyncio.run(running_agent.nats_cmd(data, wait=False))

        return "ok"

    # auto approves updates
    def approve_updates(self):
        patch_policy = self.get_patch_policy()

        updates = list()
        if patch_policy.critical == "approve":
            updates += self.winupdates.filter(
                severity="Critical", installed=False
            ).exclude(action="approve")

        if patch_policy.important == "approve":
            updates += self.winupdates.filter(
                severity="Important", installed=False
            ).exclude(action="approve")

        if patch_policy.moderate == "approve":
            updates += self.winupdates.filter(
                severity="Moderate", installed=False
            ).exclude(action="approve")

        if patch_policy.low == "approve":
            updates += self.winupdates.filter(severity="Low", installed=False).exclude(
                action="approve"
            )

        if patch_policy.other == "approve":
            updates += self.winupdates.filter(severity="", installed=False).exclude(
                action="approve"
            )

        for update in updates:
            update.action = "approve"
            update.save(update_fields=["action"])

    # returns agent policy merged with a client or site specific policy
    def get_patch_policy(self):

        # check if site has a patch policy and if so use it
        site = self.site
        core_settings = CoreSettings.objects.first()
        patch_policy = None
        agent_policy = self.winupdatepolicy.get()

        if self.monitoring_type == "server":
            # check agent policy first which should override client or site policy
            if self.policy and self.policy.winupdatepolicy.exists():
                patch_policy = self.policy.winupdatepolicy.get()

            # check site policy if agent policy doesn't have one
            elif site.server_policy and site.server_policy.winupdatepolicy.exists():
                patch_policy = site.server_policy.winupdatepolicy.get()

            # if site doesn't have a patch policy check the client
            elif (
                site.client.server_policy
                and site.client.server_policy.winupdatepolicy.exists()
            ):
                patch_policy = site.client.server_policy.winupdatepolicy.get()

            # if patch policy still doesn't exist check default policy
            elif (
                core_settings.server_policy
                and core_settings.server_policy.winupdatepolicy.exists()
            ):
                patch_policy = core_settings.server_policy.winupdatepolicy.get()

        elif self.monitoring_type == "workstation":
            # check agent policy first which should override client or site policy
            if self.policy and self.policy.winupdatepolicy.exists():
                patch_policy = self.policy.winupdatepolicy.get()

            elif (
                site.workstation_policy
                and site.workstation_policy.winupdatepolicy.exists()
            ):
                patch_policy = site.workstation_policy.winupdatepolicy.get()

            # if site doesn't have a patch policy check the client
            elif (
                site.client.workstation_policy
                and site.client.workstation_policy.winupdatepolicy.exists()
            ):
                patch_policy = site.client.workstation_policy.winupdatepolicy.get()

            # if patch policy still doesn't exist check default policy
            elif (
                core_settings.workstation_policy
                and core_settings.workstation_policy.winupdatepolicy.exists()
            ):
                patch_policy = core_settings.workstation_policy.winupdatepolicy.get()

        # if policy still doesn't exist return the agent patch policy
        if not patch_policy:
            return agent_policy

        # patch policy exists. check if any agent settings are set to override patch policy
        if agent_policy.critical != "inherit":
            patch_policy.critical = agent_policy.critical

        if agent_policy.important != "inherit":
            patch_policy.important = agent_policy.important

        if agent_policy.moderate != "inherit":
            patch_policy.moderate = agent_policy.moderate

        if agent_policy.low != "inherit":
            patch_policy.low = agent_policy.low

        if agent_policy.other != "inherit":
            patch_policy.other = agent_policy.other

        if agent_policy.run_time_frequency != "inherit":
            patch_policy.run_time_frequency = agent_policy.run_time_frequency
            patch_policy.run_time_hour = agent_policy.run_time_hour
            patch_policy.run_time_days = agent_policy.run_time_days

        if agent_policy.reboot_after_install != "inherit":
            patch_policy.reboot_after_install = agent_policy.reboot_after_install

        if not agent_policy.reprocess_failed_inherit:
            patch_policy.reprocess_failed = agent_policy.reprocess_failed
            patch_policy.reprocess_failed_times = agent_policy.reprocess_failed_times
            patch_policy.email_if_fail = agent_policy.email_if_fail

        return patch_policy

    def get_approved_update_guids(self) -> List[str]:
        return list(
            self.winupdates.filter(action="approve", installed=False).values_list(
                "guid", flat=True
            )
        )

    # returns alert template assigned in the following order: policy, site, client, global
    # will return None if nothing is found
    def get_alert_template(self) -> Union[AlertTemplate, None]:

        site = self.site
        client = self.client
        core = CoreSettings.objects.first()

        templates = list()
        # check if alert template is on a policy assigned to agent
        if (
            self.policy
            and self.policy.alert_template
            and self.policy.alert_template.is_active
        ):
            templates.append(self.policy.alert_template)

        # check if policy with alert template is assigned to the site
        elif (
            self.monitoring_type == "server"
            and site.server_policy
            and site.server_policy.alert_template
            and site.server_policy.alert_template.is_active
        ):
            templates.append(site.server_policy.alert_template)
        elif (
            self.monitoring_type == "workstation"
            and site.workstation_policy
            and site.workstation_policy.alert_template
            and site.workstation_policy.alert_template.is_active
        ):
            templates.append(site.workstation_policy.alert_template)

        # check if alert template is assigned to site
        elif site.alert_template and site.alert_template.is_active:
            templates.append(site.alert_template)

        # check if policy with alert template is assigned to the client
        elif (
            self.monitoring_type == "server"
            and client.server_policy
            and client.server_policy.alert_template
            and client.server_policy.alert_template.is_active
        ):
            templates.append(client.server_policy.alert_template)
        elif (
            self.monitoring_type == "workstation"
            and client.workstation_policy
            and client.workstation_policy.alert_template
            and client.workstation_policy.alert_template.is_active
        ):
            templates.append(client.workstation_policy.alert_template)

        # check if alert template is on client and return
        elif client.alert_template and client.alert_template.is_active:
            templates.append(client.alert_template)

        # check if alert template is applied globally and return
        elif core.alert_template and core.alert_template.is_active:
            templates.append(core.alert_template)

        # if agent is a workstation, check if policy with alert template is assigned to the site, client, or core
        elif (
            self.monitoring_type == "server"
            and core.server_policy
            and core.server_policy.alert_template
            and core.server_policy.alert_template.is_active
        ):
            templates.append(core.server_policy.alert_template)
        elif (
            self.monitoring_type == "workstation"
            and core.workstation_policy
            and core.workstation_policy.alert_template
            and core.workstation_policy.alert_template.is_active
        ):
            templates.append(core.workstation_policy.alert_template)

        # go through the templates and return the first one that isn't excluded
        for template in templates:
            # check if client, site, or agent has been excluded from template
            if (
                client.pk in template.excluded_clients.all()
                or site.pk in template.excluded_sites.all()
                or self.pk in template.excluded_agents.all()
            ):
                continue

            # see if template is excluding desktops
            if (
                self.monitoring_type == "workstation"
                and not template.agent_include_desktops
            ):
                continue
            else:
                return template

        # no alert templates found or agent has been excluded
        return None

    def generate_checks_from_policies(self):
        from automation.models import Policy

        # Clear agent checks that have overriden_by_policy set
        self.agentchecks.update(overriden_by_policy=False)

        # Generate checks based on policies
        Policy.generate_policy_checks(self)

    def generate_tasks_from_policies(self):
        from automation.models import Policy

        # Generate tasks based on policies
        Policy.generate_policy_tasks(self)

    # https://github.com/Ylianst/MeshCentral/issues/59#issuecomment-521965347
    def get_login_token(self, key, user, action=3):
        try:
            key = bytes.fromhex(key)
            key1 = key[0:48]
            key2 = key[48:]
            msg = '{{"a":{}, "u":"{}","time":{}}}'.format(
                action, user, int(time.time())
            )
            iv = get_random_bytes(16)

            # sha
            h = SHA3_384.new()
            h.update(key1)
            hashed_msg = h.digest() + msg.encode()

            # aes
            cipher = AES.new(key2, AES.MODE_CBC, iv)
            msg = cipher.encrypt(pad(hashed_msg, 16))

            return base64.b64encode(iv + msg, altchars=b"@$").decode("utf-8")
        except Exception:
            return "err"

    async def nats_cmd(self, data, timeout=30, wait=True):
        nc = NATS()
        options = {
            "servers": f"tls://{settings.ALLOWED_HOSTS[0]}:4222",
            "user": "tacticalrmm",
            "password": settings.SECRET_KEY,
            "connect_timeout": 3,
            "max_reconnect_attempts": 2,
        }
        try:
            await nc.connect(**options)
        except:
            return "natsdown"

        if wait:
            try:
                msg = await nc.request(
                    self.agent_id, msgpack.dumps(data), timeout=timeout
                )
            except ErrTimeout:
                ret = "timeout"
            else:
                ret = msgpack.loads(msg.data)

            await nc.close()
            return ret
        else:
            await nc.publish(self.agent_id, msgpack.dumps(data))
            await nc.flush()
            await nc.close()

    @staticmethod
    def serialize(agent):
        # serializes the agent and returns json
        from .serializers import AgentEditSerializer

        ret = AgentEditSerializer(agent).data
        del ret["all_timezones"]
        del ret["client"]
        return ret

    def delete_superseded_updates(self):
        try:
            pks = []  # list of pks to delete
            kbs = list(self.winupdates.values_list("kb", flat=True))
            d = Counter(kbs)
            dupes = [k for k, v in d.items() if v > 1]

            for dupe in dupes:
                titles = self.winupdates.filter(kb=dupe).values_list("title", flat=True)
                # extract the version from the title and sort from oldest to newest
                # skip if no version info is available therefore nothing to parse
                try:
                    vers = [
                        re.search(r"\(Version(.*?)\)", i).group(1).strip()
                        for i in titles
                    ]
                    sorted_vers = sorted(vers, key=LooseVersion)
                except:
                    continue
                # append all but the latest version to our list of pks to delete
                for ver in sorted_vers[:-1]:
                    q = self.winupdates.filter(kb=dupe).filter(title__contains=ver)
                    pks.append(q.first().pk)

            pks = list(set(pks))
            self.winupdates.filter(pk__in=pks).delete()
        except:
            pass

    # define how the agent should handle pending actions
    def handle_pending_actions(self):
        pending_actions = self.pendingactions.filter(status="pending")

        for action in pending_actions:
            if action.action_type == "taskaction":
                from autotasks.tasks import (
                    create_win_task_schedule,
                    enable_or_disable_win_task,
                    delete_win_task_schedule,
                )

                task_id = action.details["task_id"]

                if action.details["action"] == "taskcreate":
                    create_win_task_schedule.delay(task_id, pending_action=action.id)
                elif action.details["action"] == "tasktoggle":
                    enable_or_disable_win_task.delay(
                        task_id, action.details["value"], pending_action=action.id
                    )
                elif action.details["action"] == "taskdelete":
                    delete_win_task_schedule.delay(task_id, pending_action=action.id)

    # for clearing duplicate pending actions on agent
    def remove_matching_pending_task_actions(self, task_id):
        # remove any other pending actions on agent with same task_id
        for action in self.pendingactions.exclude(status="completed"):
            if action.details["task_id"] == task_id:
                action.delete()

    def handle_alert(self, checkin: bool = False) -> None:
        from alerts.models import Alert
        from agents.tasks import (
            agent_recovery_email_task,
            agent_recovery_sms_task,
            agent_outage_email_task,
            agent_outage_sms_task,
        )

        # return if agent is in maintenace mode
        if self.maintenance_mode:
            return

        alert_template = self.get_alert_template()

        # called when agent is back online
        if checkin:
            if Alert.objects.filter(agent=self, resolved=False).exists():

                # resolve alert if exists
                alert = Alert.objects.get(agent=self, resolved=False)
                alert.resolve()

                # check if a resolved notification should be emailed
                if (
                    not alert.resolved_email_sent
                    and alert_template
                    and alert_template.agent_email_on_resolved
                    or self.overdue_email_alert
                ):
                    agent_recovery_email_task.delay(pk=alert.pk)

                # check if a resolved notification should be texted
                if (
                    not alert.resolved_sms_sent
                    and alert_template
                    and alert_template.agent_text_on_resolved
                    or self.overdue_text_alert
                ):
                    agent_recovery_sms_task.delay(pk=alert.pk)

                # check if any scripts should be run
                if (
                    not alert.resolved_action_run
                    and alert_template
                    and alert_template.resolved_action
                ):
                    r = self.run_script(
                        scriptpk=alert_template.resolved_action.pk,
                        args=alert_template.resolved_action_args,
                        timeout=alert_template.resolved_action_timeout,
                        wait=True,
                        full=True,
                        run_on_any=True,
                    )

                    # command was successful
                    if type(r) == dict:
                        alert.resolved_action_retcode = r["retcode"]
                        alert.resolved_action_stdout = r["stdout"]
                        alert.resolved_action_stderr = r["stderr"]
                        alert.resolved_action_execution_time = "{:.4f}".format(
                            r["execution_time"]
                        )
                        alert.resolved_action_run = djangotime.now()
                        alert.save()
                    else:
                        logger.error(
                            f"Resolved action: {alert_template.resolved_action} failed to run on any agent for {self.hostname} resolved outage"
                        )

        # called when agent is offline
        else:
            # check if alert hasn't been created yet so create it
            if not Alert.objects.filter(agent=self, resolved=False).exists():

                alert = Alert.create_availability_alert(self)

                # add a null check history to allow gaps in graph
                for check in self.agentchecks.all():
                    check.add_check_history(None)
            else:
                alert = Alert.objects.get(agent=self, resolved=False)

            # create dashboard alert if enabled
            if (
                alert_template
                and alert_template.agent_always_alert
                or self.overdue_dashboard_alert
            ):
                alert.hidden = False
                alert.save()

            # send email alert if enabled
            if (
                not alert.email_sent
                and alert_template
                and alert_template.agent_always_email
                or self.overdue_email_alert
            ):
                agent_outage_email_task.delay(
                    pk=alert.pk,
                    alert_interval=alert_template.check_periodic_alert_days
                    if alert_template
                    else None,
                )

            # send text message if enabled
            if (
                not alert.sms_sent
                and alert_template
                and alert_template.agent_always_text
                or self.overdue_text_alert
            ):
                agent_outage_sms_task.delay(
                    pk=alert.pk,
                    alert_interval=alert_template.check_periodic_alert_days
                    if alert_template
                    else None,
                )

            # check if any scripts should be run
            if not alert.action_run and alert_template and alert_template.action:
                r = self.run_script(
                    scriptpk=alert_template.action.pk,
                    args=alert_template.action_args,
                    timeout=alert_template.action_timeout,
                    wait=True,
                    full=True,
                    run_on_any=True,
                )

                # command was successful
                if type(r) == dict:
                    alert.action_retcode = r["retcode"]
                    alert.action_stdout = r["stdout"]
                    alert.action_stderr = r["stderr"]
                    alert.action_execution_time = "{:.4f}".format(r["execution_time"])
                    alert.action_run = djangotime.now()
                    alert.save()
                else:
                    logger.error(
                        f"Failure action: {alert_template.action.name} failed to run on any agent for {self.hostname} outage"
                    )

    def send_outage_email(self):
        from core.models import CoreSettings

        CORE = CoreSettings.objects.first()
        alert_template = self.get_alert_template()
        CORE.send_mail(
            f"{self.client.name}, {self.site.name}, {self.hostname} - data overdue",
            (
                f"Data has not been received from client {self.client.name}, "
                f"site {self.site.name}, "
                f"agent {self.hostname} "
                "within the expected time."
            ),
            alert_template=alert_template,
        )

    def send_recovery_email(self):
        from core.models import CoreSettings

        CORE = CoreSettings.objects.first()
        alert_template = self.get_alert_template()
        CORE.send_mail(
            f"{self.client.name}, {self.site.name}, {self.hostname} - data received",
            (
                f"Data has been received from client {self.client.name}, "
                f"site {self.site.name}, "
                f"agent {self.hostname} "
                "after an interruption in data transmission."
            ),
            alert_template=alert_template,
        )

    def send_outage_sms(self):
        from core.models import CoreSettings

        alert_template = self.get_alert_template()
        CORE = CoreSettings.objects.first()
        CORE.send_sms(
            f"{self.client.name}, {self.site.name}, {self.hostname} - data overdue",
            alert_template=alert_template,
        )

    def send_recovery_sms(self):
        from core.models import CoreSettings

        CORE = CoreSettings.objects.first()
        alert_template = self.get_alert_template()
        CORE.send_sms(
            f"{self.client.name}, {self.site.name}, {self.hostname} - data received",
            alert_template=alert_template,
        )


RECOVERY_CHOICES = [
    ("salt", "Salt"),
    ("mesh", "Mesh"),
    ("command", "Command"),
    ("rpc", "Nats RPC"),
    ("checkrunner", "Checkrunner"),
]


class RecoveryAction(models.Model):
    agent = models.ForeignKey(
        Agent,
        related_name="recoveryactions",
        on_delete=models.CASCADE,
    )
    mode = models.CharField(max_length=50, choices=RECOVERY_CHOICES, default="mesh")
    command = models.TextField(null=True, blank=True)
    last_run = models.DateTimeField(null=True, blank=True)

    def __str__(self):
        return f"{self.agent.hostname} - {self.mode}"

    def send(self):
        ret = {"recovery": self.mode}
        if self.mode == "command":
            ret["cmd"] = self.command
        return ret


class Note(models.Model):
    agent = models.ForeignKey(
        Agent,
        related_name="notes",
        on_delete=models.CASCADE,
    )
    user = models.ForeignKey(
        "accounts.User",
        related_name="user",
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
    )
    note = models.TextField(null=True, blank=True)
    entry_time = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.agent.hostname
