from datetime import datetime
import operator
from threading import Lock
from time import sleep

import attr
import psutil

from apiserver.utilities.threads_manager import ThreadsManager


stat_threads = ThreadsManager("Statistics")


@attr.s(auto_attribs=True)
class Sample:
    cpu_usage: float = 0.0
    mem_used_gb: float = 0
    mem_free_gb: float = 0

    @classmethod
    def _apply(cls, op, *samples):
        return cls(
            **{
                field: op(*(getattr(sample, field) for sample in samples))
                for field in attr.fields_dict(cls)
            }
        )

    def min(self, sample):
        return self._apply(min, self, sample)

    def max(self, sample):
        return self._apply(max, self, sample)

    def avg(self, sample, count):
        res = self._apply(lambda x: x * count, self)
        res = self._apply(operator.add, res, sample)
        res = self._apply(lambda x: x / (count + 1), res)
        return res

    @classmethod
    def get_current_sample(cls) -> "Sample":
        return cls(
            cpu_usage=psutil.cpu_percent(),
            mem_used_gb=psutil.virtual_memory().used / (1024 ** 3),
            mem_free_gb=psutil.virtual_memory().free / (1024 ** 3),
        )


class ResourceMonitor:
    class Accumulator:
        def __init__(self):
            sample = Sample.get_current_sample()
            self.avg = sample
            self.min = sample
            self.max = sample
            self.time = datetime.utcnow()
            self.count = 1

        def add_sample(self, sample: Sample):
            self.min = self.min.min(sample)
            self.max = self.max.max(sample)
            self.avg = self.avg.avg(sample, self.count)
            self.count += 1

    sample_interval_sec = 5
    _lock = Lock()
    accumulator = Accumulator()

    @classmethod
    @stat_threads.register("resource_monitor", daemon=True)
    def start(cls):
        while True:
            sleep(cls.sample_interval_sec)
            sample = Sample.get_current_sample()
            with cls._lock:
                cls.accumulator.add_sample(sample)

    @classmethod
    def get_stats(cls) -> dict:
        """ Returns current resource statistics and clears internal resource statistics """
        with cls._lock:
            min_ = attr.asdict(cls.accumulator.min)
            max_ = attr.asdict(cls.accumulator.max)
            avg = attr.asdict(cls.accumulator.avg)
            interval = datetime.utcnow() - cls.accumulator.time
            cls.accumulator = cls.Accumulator()

        return {
            "interval_sec": interval.total_seconds(),
            "num_cores": psutil.cpu_count(),
            **{
                k: {"min": v, "max": max_[k], "avg": avg[k]}
                for k, v in min_.items()
            }
        }