import logging
import traceback

from croniter import croniter
from sqlalchemy.sql import null
from datetime import datetime, timedelta, timezone

from wurker.util import init_logger
from wurker.models import WurkerBeeReport, WurkerBeeState


class BaseBee:
    """Base class for Wurker Bees. Implement if you want, but it's basic."""

    # Hive process stuff
    pid = None
    last_pid = None
    proc = None
    errors = None
    output = None
    start_dt = None
    runcount = 0
    proc_timeout = 60

    # The DB session
    session = None

    # The DBee object and optional DB cron object
    dbee = None
    dcron = None

    # The logger to use
    logger = None

    # Straight from the DBee
    id = None
    app_id = ""
    name = ""
    slug = ""
    description = ""
    command_id = None
    args = None
    quiet_restart = False
    enabled = True

    # Depending on wurker_command
    module = None
    module_name = ""
    command = None
    command_slug = ""

    # If scheduled, from the DBee or its cron
    cron_id = None
    cron_slug = ""
    cron = ""
    overrun = 0

    # If scheduled, computed from cron
    timeout = None
    itercron = None
    curr_run = None
    next_run = None

    # Local param, needed to run once from CLI
    run_once = False

    # The wurker_bee_queue object for this run
    queue = None

    def __init__(self, session, dbee, dcron=None, args=None, logger=None):
        """Init from DBee and related objects."""

        self.session = session
        self.dbee = dbee
        self.dcron = dcron
        self.logger = logger or init_logger()

        # Init Bee attributes from DBee
        self.id = dbee.id
        self.app_id = dbee.app_id
        self.name = dbee.name
        self.slug = dbee.slug
        self.description = dbee.description
        self.command_id = dbee.command_id
        self.args = args or dbee.args
        self.quiet_restart = dbee.quiet_restart
        self.enabled = dbee.enabled

        self.command_slug = dbee.command.slug
        if dbee.command.is_module:
            self.module_name = dbee.command.command
        else:
            self.command = dbee.command.command

        if dbee.queue is not None:
            self.queue = dbee.queue

        # Additional inits
        self._init_cron()
        self._init_timeout()

    def _init_cron(self):
        """Init self-scheduling iteration from cron, if it exists."""

        if self.dbee is None or self.dcron is None:
            return

        # Assign the actual cron string like '0/5 * * * *' to the 'cron' attribute
        self.cron_id = self.dcron.id
        self.cron_slug = self.dcron.slug
        self.cron = self.dcron.cron
        self.overrun = self.dcron.overrun

        if self.cron is None:
            return

        self.itercron = croniter(self.cron, self.Now)
        if not self.next_run:
            self.set_next()

    def _init_timeout(self):
        """If None, then a reasonable default timeout based on itercron interval."""

        if isinstance(self.timeout, int) or not self.cron:
            return

        itercron = croniter(self.cron, self.Now)
        next_0 = itercron.get_next(datetime)
        next_1 = itercron.get_next(datetime)
        duration = int((next_1 - next_0).total_seconds())

        # Buffer: 10 sec for < 5 min; 30 sec for >= 5 min (300/10=30); 1 min for >= 10 min (600/10=60)
        buff = (
            10 if duration < 300 else
            30 if 300 <= duration < 600 else
            60
        )
        self.timeout = duration - buff

    def reset_proc(self):
        """Reset process attrs."""

        self.proc = None
        self.last_pid = self.pid
        self.pid = None

    @property
    def PID(self):
        return self.pid or self.last_pid

    @property
    def Now(self):
        return datetime.now(timezone.utc)

    @property
    def CurrRun(self):
        """A property for curr_run so it looks like NextRun."""

        return self.curr_run

    @property
    def NextRun(self):
        """A property to make sure next_run is populated."""

        if not self.next_run or self.next_run <= self.Now.replace(second=0, microsecond=0) - timedelta(minutes=1):
            self.next_run = self.get_next()

        return self.next_run

    def get_next(self):
        """Get the next iteration over croniter."""

        next_run = self.itercron.get_next(datetime)

        while next_run <= self.Now.replace(second=0, microsecond=0):
            next_run = self.itercron.get_next(datetime)

        return next_run

    def set_next(self):
        """Advance the iteration over croniter, saving current run."""

        self.curr_run = self.next_run
        self.next_run = self.get_next()

    @property
    def AsDict(self):
        """Return the Bee config as a dict"""

        return {
            "id": self.id,
            "app_id": self.app_id,
            "name": self.name,
            "slug": self.slug,
            "description": self.description,
            "command_id": self.command_id,
            "command_slug": self.command_slug,
            "args": self.args,
            "timeout": self.timeout,
            "quiet_restart": self.quiet_restart,
            "enabled": self.enabled,
            "module_name": self.module_name,
            "command": self.command,
            "cron_id": self.cron_id,
            "cron": self.cron,
        }

    def log(self, msg, exception=False, level=logging.INFO, prefix=''):
        """Adds Bee-specific metadata to log entries."""

        if exception:
            level = logging.ERROR
            msg += "\n{0}".format(traceback.format_exc())

        prefix = prefix or self.slug
        self.logger.log(msg, level=level, prefix=prefix)

    def set_report(self, status=WurkerBeeState.PROCESSING, status_info=None):
        """Set report status, possibly creating a new report record first."""

        with self.session() as db:
            report = db.query(WurkerBeeReport).filter(
                WurkerBeeReport.app_id == self.app_id,
                WurkerBeeReport.wurker_bee_id == self.id,
                WurkerBeeReport.wurker_cron_id == self.cron_id,
                WurkerBeeReport.hive_pid == self.pid,
            ).first()

            if report is None:
                report = WurkerBeeReport(
                    app_id = self.app_id,
                    wurker_bee_id = self.id,
                    wurker_cron_id = self.cron_id,
                    args = self.args,
                )
                db.add(report)

            report.status = status
            self._update_hive_info(report)

            # The status_info column is JSON datatype, so needs special handling
            report_info = self._update_status_info(report, status_info)
            report.status_info = null()
            db.flush()
            report.status_info = report_info

            # Notice we didn't commit until now!
            db.commit()

    def _update_hive_info(self, report):
        """Particulars about this Hive run of the Bee. We ignore duration and instead compute it."""

        if not report or self.pid is None:
            return

        # Compute duration if hive_info already set
        if report.hive_pid is not None and report.hive_timestamp is not None:
            if report.hive_timestamp.tzinfo is None:
                report.hive_timestamp = report.hive_timestamp.replace(tzinfo=timezone.utc)

            report.hive_duration = (self.Now - report.hive_timestamp).total_seconds()

        # Compute hive values now
        report.hive_pid = self.pid
        report.hive_timestamp = self.Now       # Important: we did this AFTER setting duration!
        report.hive_runcount = self.runcount

    def _update_status_info(self, report, status_info):
        """Just easier to return from a little function than nest if-elifs."""

        report_info = report.status_info or {}

        if not report_info:
            report_info = status_info or {"runcount": self.runcount}

        if not status_info:
            return report_info

        # Info we might wanna update in-place...
        if "errors" in status_info:
            if "errors" not in report_info:
                report_info["errors"] = status_info["errors"]
            else:
                report_info["errors"].extend(status_info["errors"])

        if "runcount" in status_info:
            report_info["runcount"] = status_info["runcount"]
        else:
            report_info["runcount"] = self.runcount

        # Arbitrary values...
        for key, value in status_info.items():
            if key in ["errors", "runcount"]:
                continue

            report_info[key] = value

        # JSON datatype so we return the data to be nullified, flushed and set again
        return report_info

    def run(self):
        raise NotImplementedError

    def poll(self):
        raise NotImplementedError

    def wait(self, timeout=None):
        raise NotImplementedError

    def kill(self):
        raise NotImplementedError

    def __repr__(self):
        return "%s(%r)" % (self.__class__, self.__dict__)
