#!/usr/bin/env python3

"""
Usage:
    fsm-step YOUR.MODULE [retry_id]
    fsm-step XXX.py [retry_id]

fsm-step will load custom outer module and call the function *main*

the function main should return
    `a string represents next_state`
OR
    `a tuple represents (next_state, patch_data)`
"""

import importlib
import json
import os
import sys
import time

from http import client
from urllib.parse import urlparse


class Api:
    def __init__(self, url):
        url = urlparse(url)
        if url.scheme == "http":
            self.http = client.HTTPConnection(url.netloc)
        elif url.scheme == "https":
            self.http = client.HTTPSConnection(url.netloc)
        else:
            raise ValueError(url)

    def get(self, id):
        self.http.request("GET", f"/{id}")
        r = self.http.getresponse()
        if r.status > client.NO_CONTENT:
            raise client.HTTPException(r.status)
        return json.load(r)

    def _post(self, path, data=None):
        body = data and json.dumps(data)
        self.http.request("POST", path, body, headers={
                          "Content-Type": "application/json"})
        r = self.http.getresponse()
        if r.status > client.NO_CONTENT:
            raise client.HTTPException(r.status)
        o = r.read()
        if o:
            return json.loads(o)

    def new(self, state, initial_data: dict = None):
        return self._post(f"/new/{state}", initial_data)

    def lock(self, state):
        return self._post(f"/lock/{state}?wait=yes")

    def transit(self, id, next_state, patch_data: dict = None):
        return self._post(f"/transit/{id}/{next_state}", patch_data)


api = Api(os.environ.get("FSMHUB") or "http://fsmhub")


try:
    from loguru import logger
except ImportError:
    import logging as logger

sentry_sdk = None
_sentry_dsn = os.environ.get("SENTRY")
if _sentry_dsn:
    try:
        import sentry_sdk
        sentry_sdk.init(dsn=_sentry_dsn)
    except ImportError:
        logger.warning("*sentry-sdk* is not installed")


sys.path.append("")
module_name = sys.argv[1]
if module_name.endswith(".py"):
    module_name = module_name[:-3].replace("/", ".")

# you MUST define the function *main*
that_function = importlib.import_module(module_name).main


def call(kwargs: dict):
    """call logic function, use magic
    """
    c = that_function.__code__
    args = [kwargs.pop(c.co_varnames[i], None) for i in range(c.co_argcount)]
    if len(c.co_varnames) == c.co_argcount:
        return that_function(*args)
    return that_function(*args, **kwargs)


def transit(id, result):
    if isinstance(result, str):
        next_state, patch_data = result, None
    else:
        next_state, patch_data = result
        assert isinstance(patch_data, dict), patch_data
    logger.debug(next_state)
    patch_data and logger.debug(patch_data)
    api.transit(id, next_state, patch_data)


def main():
    """
    """
    if len(sys.argv) > 2:  # this mode is for fix or debug
        retry_id = int(sys.argv[2])
        payload = api.get(retry_id)
        logger.debug(payload)
        state = payload["state"]
        assert state.endswith(module_name) and state != module_name, state
        result = call(payload["data"] or {})
        transit(retry_id, result)
        return

    # loop mode
    while True:
        payload = api.lock(module_name)
        logger.debug(payload)
        id = payload["id"]
        kwargs = payload["data"] or {}
        assert payload["state"] == module_name

        try:
            result = call(kwargs)
        except Exception as e:
            logger.exception(f"{module_name}.main")
            sentry_sdk and sentry_sdk.capture_exception(e)
            continue

        transit(id, result)


if __name__ == '__main__':
    main()
