import ska_control_model as scm
import ska_tango_base as stb
import tango
import tango.server
import watchfiles
import release

import os
import pwd
import grp
import time
import stat
import threading
import logging
import typing

# Disable watchfiles info logs as they can be quite noisy
logging.getLogger("watchfiles.main").setLevel(logging.WARNING)


class FileStats(stb.base.BaseInterface):
    """Tango device that exposes file statistics as attributes."""

    FilePath = tango.server.device_property(
        dtype="DevString", default_value="dummy", doc="Path of file to monitor"
    )

    def init_device(self) -> None:
        """Initialise the device."""
        super().init_device()

        self._version_id = release.version
        self._build_state = f"{release.name} {release.version}: {release.description}"
        self.metadata = Metadata(self.FilePath, self.logger)

        self.stop_event: threading.Event | None = None
        self.monitor_thread: threading.Thread | None = None

        self.init_completed()

    def delete_device(self) -> None:
        """De-initialise device."""
        self._stop_monitor_thread()
        super().delete_device()

    def change_control_level(self, control_level: stb.base.ControlLevel) -> None:
        """Change how the device is interacting with the system under control."""
        if control_level == stb.base.ControlLevel.NO_CONTACT:
            self._stop_monitor_thread()
            self.metadata.reset()
            self.component_disconnected()
        elif control_level == stb.base.ControlLevel.FULL_CONTROL:
            self.component_on()
            self._start_monitor_thread()
        else:
            raise ValueError(f"Unknown control_level {control_level}")

    def _start_monitor_thread(self) -> None:
        def target(device: FileStats) -> None:
            try:
                assert device.stop_event is not None
                device._status = f"Monitoring '{self.FilePath}'"
                device.metadata.monitor_for_updates(device.stop_event)
            except Exception as ex:
                device.logger.exception("File monitor raised unexpected exception")
                device.component_fault()
                device._status = f"Monitor thread crashed: {ex}"
                return
            device._status = f"Monitoring disabled"

        self.stop_event = threading.Event()
        self.monitor_thread = threading.Thread(target=target, args=(self,))
        self.monitor_thread.start()

    def _stop_monitor_thread(self) -> None:
        if self.stop_event is not None:
            assert self.monitor_thread is not None

            self.stop_event.set()
            self.monitor_thread.join()

            self.monitor_thread = None
            self.stop_event = None

    def read_attr_hardware(self, attr_list: list[int]) -> None:
        """Prepare for attribute read."""
        _ = attr_list
        if self.get_state() != tango.DevState.DISABLE:
            self.metadata.refresh()

    @tango.server.attribute(dtype=int)
    def size(self) -> stb.type_hints.ReadAttrType[int]:
        """Read the size of the file in bytes."""
        return self._to_read_attr_type(self.metadata.size, 0)

    @tango.server.attribute(dtype=str)
    def lastModifiedTime(self) -> stb.type_hints.ReadAttrType[str]:
        """Return the time the file was last modified."""
        return self._to_read_attr_type(self.metadata.last_modified_time, "")

    @tango.server.attribute(dtype=str)
    def owner(self) -> stb.type_hints.ReadAttrType[str]:
        """Return the owner of the file."""
        return self._to_read_attr_type(self.metadata.owner, "")

    @tango.server.attribute(dtype=str)
    def mode(self) -> stb.type_hints.ReadAttrType[str]:
        """Return the mode (permissions) of the file."""
        return self._to_read_attr_type(self.metadata.mode, "")

    def _to_read_attr_type(
        self, value: typing.Any, default: typing.Any
    ) -> stb.type_hints.ReadAttrType[typing.Any]:
        if value is None:
            return default, time.time(), tango.AttrQuality.ATTR_INVALID

        return value, self.metadata.timestamp, tango.AttrQuality.ATTR_VALID


class Metadata:
    """File metadata."""

    def __init__(self, path: str, logger: logging.Logger) -> None:
        self.path = os.path.abspath(path)
        self.logger = logger
        self.data: os.stat_result | None = None
        self.timestamp: float | None = None

    def reset(self) -> None:
        """Reset metadata to None."""
        self.data = None
        self.timestamp = None

    def refresh(self) -> None:
        """Refresh metadata."""
        self.data = os.stat(self.path)
        self.timestamp = time.time()

    def monitor_for_updates(self, stop_event: threading.Event) -> None:
        """Setup monitoring for file metadata changes."""
        parent = os.path.dirname(self.path)

        def only_filename(change: watchfiles.Change, name: str) -> bool:
            _ = change
            return os.path.abspath(name) == self.path

        # Monitor from parent directory to know if the file is created or deleted.
        # TODO: Support missing parent directory
        watch = watchfiles.watch(
            parent, watch_filter=only_filename, stop_event=stop_event
        )

        # Refresh after we start watch to ensure we are up-to-date and
        # don't miss a change
        self.refresh()

        self.logger.debug("Watching %s", parent)
        for changes in watch:
            self.logger.debug("Got changes=%s", changes)
            self.refresh()

    @property
    def size(self) -> int | None:
        """Return the size of the file in bytes."""
        if self.data is not None:
            return self.data.st_size
        return None

    @property
    def mode(self) -> str | None:
        """
        Return the mode (permission) of the file.

        :return: mode in the same format as ``ls -l``.
        """
        if self.data is not None:
            return stat.filemode(self.data.st_mode)
        return None

    @property
    def owner(self) -> str | None:
        """
        Return the owner of the file.

        :return: has the format "<user>:<group>"
        """
        if self.data is not None:
            user = pwd.getpwuid(self.data.st_uid)
            group = grp.getgrgid(self.data.st_gid)
            return f"{user.pw_name}:{group.gr_name}"
        return None

    @property
    def last_modified_time(self) -> str | None:
        """
        Return the time the file was last modified.

        :return: last modified time in the ``ctime`` format
        """
        if self.data is not None:
            return time.ctime(self.data.st_mtime)
        return None


if __name__ == "__main__":
    tango.server.run((FileStats,))
