#!/opt/imunify360/venv/bin/python3
import asyncio
import os
import itertools
from logging import getLogger
from pathlib import Path

from peewee import OperationalError
from playhouse.sqlite_ext import SqliteExtDatabase

from im360.contracts.config import IPSET_LISTS_PATH
from imav.malwarelib.subsys.malware import (
    subscribe_to_malware_action,
    HackerTrapHitsSaver,
)
from imav.internals.lazy_load import AVSource
from imav import server
from defence360agent.subsys import systemd_notifier
from defence360agent.api import health
from defence360agent.contracts.config import Model
from defence360agent.contracts.license import LicenseCLN
from defence360agent.contracts.plugins import BasePlugin
from defence360agent.internals.cln import subscribe_to_license_changes
from defence360agent.internals.iaid import IndependentAgentIDAPI
from defence360agent.internals.lazy_load import CoreSource
from defence360agent.model import instance
from defence360agent.router import Router
from defence360agent.application import app
from imav.run import AV_PLUGINS_PACKAGES
from defence360agent.utils import importer, Scope, fail_agent_service

from im360 import rpc_handlers
from im360.application.settings import configure
from im360.internals.core import ip_versions
from im360.internals.lazy_load import IM360Source
from im360.subsys import features

IM360_PLUGINS_PACKAGES = ("im360.plugins",)
logger = getLogger(__name__)


def _set_correct_fgw_plugin(plugin):
    #  TEMPORARY LOGIC FOR MIGRATE TO FGW PLUGIN
    #  HANDLE CASE WHEN HTTP VERSION OF SendToServerClient MUST BE USER
    if plugin.__name__ == "SendToServerFGW":
        # NATS takes priority when enabled
        if os.getenv("I360_NATS_ENABLED", "true").lower() == "true":
            from im360.nats_gateway import SendToServerNATS

            logger.info("Using NATS gateway for message delivery")
            return SendToServerNATS

        flag_file_path = os.getenv(
            "IM360_FGW_FOLDER_PATH", "/var/imunify360/.dont.use.fgw.flag"
        )
        if Path(flag_file_path).exists():
            from defence360agent.plugins.client import SendToServer

            return SendToServer
    return plugin


def get_plugins() -> set:
    """Return plugins in unspecified order."""
    importer.load_packages(
        CoreSource.MESSAGES + AVSource.MESSAGES + IM360Source.MESSAGES
    )
    importer.load_packages(AV_PLUGINS_PACKAGES + IM360_PLUGINS_PACKAGES)
    # use lexicographical order (but don't rely on it in code)
    return sorted(
        {
            _set_correct_fgw_plugin(plugin)
            for plugin in BasePlugin.get_active_plugins()
            if plugin.SCOPE not in (Scope.AV, Scope.IM360_RESIDENT)
        },
        key=lambda item: f"{item.__module__}.{item.__name__}",
    )


async def update_health_sensor():
    if LicenseCLN.is_valid():
        health.sensor.registered()
    else:
        health.sensor.unregistered()


async def attach_db(
    db: SqliteExtDatabase, db_path: str, schema_name: str
) -> None:
    max_attempts = 5
    for attempt in itertools.count(1):
        try:
            db.execute_sql("ATTACH ? AS ?", (db_path, schema_name))
            return
        except Exception as e:
            if isinstance(e, OperationalError) and attempt < max_attempts:
                await asyncio.sleep(5)
                continue

            logger.error(
                "Error attaching to database",
                extra={
                    "db_path": db_path,
                    "schema_name": schema_name,
                    "attempt": attempt,
                },
            )
            raise


async def setup_databases():
    instance.db.execute_sql(
        "ATTACH ? AS ?", (Model.PROACTIVE_PATH, "proactive")
    )
    # DB is created and migration applied in resident part
    # so we need to retry until it's ready
    # 5 attempts with 5 seconds sleep between them max 20 seconds, concurrently for each database
    async with asyncio.TaskGroup() as task_group:
        task_group.create_task(
            attach_db(instance.db, Model.RESIDENT_PATH, "resident")
        )
        task_group.create_task(
            attach_db(instance.db, IPSET_LISTS_PATH, "ipsetlists")
        )

    router = Router(
        instance.db,
        migrations_dirs=app.MIGRATIONS_DIRS,
        logger=logger,
    )
    # Do not run non-resident part if there are new migration in dirs
    # This behavior might be reproduced on update
    # non-resident and resident agents both would be restarted
    # as soon as update process is finished
    if router.diff:
        logger.error("Tried to start while migrations are not applied")
        fail_agent_service()


async def init_actions():
    # Any uncaught exceptions here prevents the agent from
    # starting. Non-critical (agent can continue) functionality should
    # either be moved to a plugin or caught&log *all* its exceptions.

    # Also nothing should block here indefinitely (any operation that
    # may block should have a timeout).

    ip_versions.init()
    subscribe_to_license_changes(features.update_repos)
    subscribe_to_license_changes(features.update_im_email)
    subscribe_to_license_changes(update_health_sensor)

    await setup_databases()

    subscribe_to_malware_action("delete", HackerTrapHitsSaver.add_hit)
    subscribe_to_malware_action("cleanup", HackerTrapHitsSaver.add_hit)
    subscribe_to_license_changes(IndependentAgentIDAPI.reactivate)
    IndependentAgentIDAPI.add_initial_task()
    rpc_handlers.init()


def run():
    systemd_notifier.notify(systemd_notifier.AgentState.READY)
    configure()
    plugins = get_plugins()
    server.start(plugins, init_actions)


if __name__ == "__main__":
    run()
    logger.info("agent stopped")
