# NEON AI (TM) SOFTWARE, Software Development Kit & Application Framework
# All trademark and other rights reserved by their respective owners
# Copyright 2008-2025 Neongecko.com Inc.
# Contributors: Daniel McKnight, Guy Daniels, Elon Gasper, Richard Leeds,
# Regina Bloomstine, Casimiro Ferreira, Andrii Pernatii, Kirill Hrymailo
# BSD-3 License
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# 1. Redistributions of source code must retain the above copyright notice,
#    this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation
#    and/or other materials provided with the distribution.
# 3. Neither the name of the copyright holder nor the names of its
#    contributors may be used to endorse or promote products derived from this
#    software without specific prior written permission.
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
# CONTRIBUTORS  BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
# OR PROFITS;  OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE,  EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import tracemalloc

from threading import Event, Thread
from typing import Optional
from ovos_utils.log import LOG
from ovos_utils.process_utils import ProcessStatus, ProcessState

_malloc_event = Event()
_malloc_thread = None


def start_systemd_service(service: callable, **kwargs):
    """
    Start a Neon Core module with systemd wrappers to report process lifecycle
    """
    try:
        import sdnotify
    except ImportError:
        LOG.exception(
            "sdnotify not installed! Starting service without systemd hooks"
        )
        service(**kwargs)
        return
    notifier = sdnotify.SystemdNotifier()

    def on_ready():
        notifier.notify("READY=1")
        notifier.notify("STATUS=Ready")

    def on_stopping():
        try:
            stop_malloc()
        except Exception as e:
            LOG.error(e)
        notifier.notify("STOPPING=1")
        notifier.notify("STATUS=Stopping")

    def on_error(err: str):
        if err.isnumeric():
            notifier.notify(f"ERRNO={err}")
        else:
            notifier.notify("ERRNO=1")

    def on_alive():
        notifier.notify("STATUS=Starting")

    def on_started():
        notifier.notify("STATUS=Started")

    service(
        ready_hook=on_ready,
        error_hook=on_error,
        stopping_hook=on_stopping,
        alive_hook=on_alive,
        started_hook=on_started,
        **kwargs,
    )


def start_health_check_server(
    service_status: ProcessStatus, port: int = 8000,
    status_callback: Optional[callable] = None
) -> Thread:
    """
    Starts an HTTP server to report the status of a module that implements a
    ProcessStatus object. A specified `status_callback` is expected to modify
    the `service_status` object as any returned value is ignored. If the
    `status_callback` raises an exception, the `service_status` will be
    updated here to set the error state.

    :param service_status: ProcessStatus object to get status from
    :param port: Port to run the health check server on
    :param status_callback: Optional callback to run on each health check
    """
    import json
    from http.server import BaseHTTPRequestHandler, HTTPServer

    class HealthCheckHandler(BaseHTTPRequestHandler):
        def log_message(self, format, *args):
            # Override logging to use LOG singleton
            LOG.debug(format % args)

        def do_GET(self):
            if self.server.status_callback is not None:
                try:
                    self.server.status_callback()
                except Exception as e:
                    self.server.service_status.set_error(str(e))
            if self.path == "/status":
                if self.server.service_status.state == ProcessState.NOT_STARTED:
                    resp_code = 503
                    content = "Service not started"
                elif self.server.service_status.state == ProcessState.STARTED:
                    resp_code = 200
                    content = "Starting"
                elif self.server.service_status.state == ProcessState.ALIVE:
                    resp_code = 200
                    content = "Starting"
                elif self.server.service_status.state == ProcessState.READY:
                    resp_code = 200
                    content = "Ready"
                elif self.server.service_status.state == ProcessState.STOPPING:
                    resp_code = 503
                    content = "Stopping"
                elif self.server.service_status.state == ProcessState.ERROR:
                    resp_code = 500
                    content = "Error"
                else:
                    resp_code = 500
                    content = f"Unknown state: {self.service_status.state}"

                self.send_response(resp_code)
                self.send_header("Content-Type", "application/json")
                self.end_headers()
                self.wfile.write(
                    json.dumps({"status": content}).encode("utf-8")
                )
            else:
                self.send_response(404)
                self.end_headers()

    server = HTTPServer(("0.0.0.0", port), HealthCheckHandler)
    server.service_status = service_status
    server.status_callback = status_callback
    thread = Thread(target=server.serve_forever, daemon=True)
    thread.start()
    LOG.info(f"Started health check endpoint at {server.server_address}")
    return thread


def start_malloc(
    config: dict = None, stack_depth: int = 1, force: bool = False
) -> bool:
    """
    Start malloc trace if configured
    :param config: dict Configuration
    :param stack_depth: depth to track memory usage
    :param force: if True, start tracemalloc regardless of configuration
    :returns: True if malloc started
    """
    if not config:
        from ovos_config.config import Configuration

        config = Configuration()
    if (
        force
        or config.get("debugging")
        and config["debugging"].get("tracemalloc")
    ):
        LOG.info("starting tracemalloc")
        tracemalloc.start(stack_depth)
        if config["debugging"].get("log_malloc"):
            interval_minutes = config["debugging"].get(
                "log_interval_minutes", 60
            )
            global _malloc_thread
            _malloc_thread = Thread(
                target=_log_malloc,
                args=((interval_minutes * 60),),
                daemon=True,
            )
            _malloc_thread.start()
        return True
    return False


def stop_malloc():
    """
    Stop tracemalloc logging if active
    """
    if _malloc_thread:
        LOG.debug("Stopping malloc logging")
        _malloc_event.set()
        _malloc_thread.join()


def snapshot_malloc() -> Optional[tracemalloc.Snapshot]:
    """
    Capture and return a tracemalloc Snapshot if tracemalloc is started
    """
    try:
        LOG.debug("Capturing malloc snapshot")
        return tracemalloc.take_snapshot()
    except RuntimeError:
        LOG.debug("No tracemalloc trace")
    except Exception as e:
        LOG.exception(e)
    return None


def print_malloc(
    snapshot: tracemalloc.Snapshot, limit: int = 8, filter_traces: bool = False
):
    """
    Log a malloc snapshot
    :param snapshot: Snapshot to evaluate
    :param limit: number of traces to log
    :param filter_traces: if True, remove importlib and unknown stack traces
    """
    LOG.debug("Processing snapshot")
    if filter_traces:
        snapshot = snapshot.filter_traces(
            (
                tracemalloc.Filter(False, "<frozen importlib._bootstrap>"),
                tracemalloc.Filter(
                    False, "<frozen importlib._bootstrap_external>"
                ),
                tracemalloc.Filter(False, "<unknown>"),
            )
        )
    top_stats = snapshot.statistics("traceback")

    LOG.info(f"Top {limit} memory users")
    for index, stat in enumerate(top_stats[:limit], 1):
        frame = stat.traceback[0]
        LOG.info(
            f"#{index}: {frame.filename}:{frame.lineno}: "
            f"{stat.size / 1048576} MiB"
        )
        for frame in stat.traceback[1:]:
            LOG.debug(f"{frame.filename}:{frame.lineno}")

    other = top_stats[limit:]
    if other:
        size = sum(stat.size for stat in other)
        LOG.info(f"{len(other)} other:{size / 1048576} MiB")
    total = sum(stat.size for stat in top_stats)
    LOG.info(f"Total allocated size: {total / 1048576} MiB")


def _log_malloc(interval_seconds: int):
    _malloc_event.clear()
    while not _malloc_event.wait(interval_seconds):
        print_malloc(snapshot_malloc(), filter_traces=True)
    LOG.info("Stopping malloc logging")
