import argparse
from collections import deque
import json
import logging
import sys
import time


class Alarm(object):
  def __init__(self, cfg, proxy, poll_interval):
    self.metric = proxy.metrics[str(cfg["metric"])]
    self.low_value = int(cfg["low-value"])
    self.high_value = int(cfg["high-value"])
    self.period = int(cfg["period"])
    self.scale_n = int(cfg["scale-nodes"])
    avg = self.low_value + (self.high_value - self.low_value) // 2
    self.window = deque([avg] * (self.period // poll_interval))

  def __repr__(self):
    return ("%s (lo=%s, hi=%s, p=%i, n=%i)" %
            (self.metric, str(self.low_value), str(self.high_value),
             self.period, self.scale_n))

  def update(self):
    self.window.append(self.metric.value)
    self.window.popleft()

  def trigger_low(self):
    return max(self.window) < self.low_value

  def trigger_high(self):
    return min(self.window) > self.high_value


class Monitor(object):
  def __init__(self, cfg):
    self.proxy  = load_class(cfg, "load-balancer")
    self.cloud  = load_class(cfg, "cloud-provider")
    self.deploy = load_class(cfg, "deployment-strategy")
    self.poll_interval = cfg["load-monitor"]["poll-interval"]
    self.cooldown_period = cfg["load-monitor"]["cooldown-period"]
    self.cooldown = 0
    self.alarms = []
    for alarm_cfg in cfg["load-monitor"]["alarms"]:
      self.alarms.append(Alarm(alarm_cfg, self.proxy, self.poll_interval))

  def start_cooldown(self):
    self.cooldown = self.cooldown_period
    logging.info("Cooldown period started (%i s)", self.cooldown_period)

  def update_cooldown(self):
    self.cooldown -= self.poll_interval
    if not self.cooldown:
      logging.info("Cooldown period ended (%i s)", self.cooldown_period)

  def scale_up(self, n):
    if not n: return
    self.cloud.start_nodes(n, self.deploy.get_bootstrap_script())
    logging.warning("Scaling up: starting %i nodes" % n)
    self.start_cooldown()

  def scale_down(self, n):
    if not n: return
    stopped = self.cloud.stop_nodes(n)
    if not len(stopped): return
    logging.warning("Scaling down: stopping %i nodes" % len(stopped))
    self.start_cooldown()
    self.proxy.remove_nodes(stopped)
    self.deploy.remove_nodes(stopped)

  def run(self):
    self.alarms.sort(key=lambda a: a.scale_n, reverse=True)
    while True:
      self.proxy.update_metrics()
      for alarm in self.alarms:
        alarm.update()
        logging.info(alarm)
      if not self.cooldown:
        for alarm in self.alarms:
          if alarm.trigger_high():
            self.scale_up(alarm.scale_n)
            break
          elif alarm.trigger_low():
            self.scale_down(alarm.scale_n)
            break
      started = self.cloud.get_started_nodes()
      self.proxy.add_nodes(started)
      self.deploy.add_nodes(started)
      time.sleep(self.poll_interval)
      if self.cooldown:
        self.update_cooldown()


def load_class(cfg, name):
  (module_name, class_name) = str(cfg[name]["type"]).rsplit(".", 1)
  module = __import__(module_name, fromlist=[class_name])
  return getattr(module, class_name)(cfg)


def main():
  parser = argparse.ArgumentParser()
  parser.add_argument("-c", "--config", required=True,
                      help="configuration file")
  parser.add_argument("-q", "--quiet", action="store_true",
                      help="suppress log messages")
  args = parser.parse_args()

  if args.quiet:
    logging.basicConfig(level=logging.ERROR)
  else:
    logging.basicConfig(format="%(asctime)s [%(levelname)s] %(message)s",
                        level=logging.INFO)

  cfg = json.load(open(args.config))
  mon = Monitor(cfg)
  mon.run()
