import argparse
import asyncio
import os
import random
from concurrent.futures import ThreadPoolExecutor
from io import StringIO
from pathlib import Path
from typing import Callable

from clarabot_nano_common.common.config.constants import CommonConfigConstants
from clarabot_nano_common.common.protocol.errors import (
    NANO_REQUEST_ERROR_HANDLER_TEMPORARY_FAILURE,
    NANO_REQUEST_ERROR_HANDLER_RESET_REQUEST,
    NANO_REQUEST_ERROR_PART_RANGE,
    NANO_REQUEST_ERROR_CONFLICT,
    NANO_REQUEST_ERROR_TOO_MANY_REQUESTS,
)
from clarabot_nano_common.common.protocol.nano_request import NanoResponseStreamProcessor, NanoResponseError
from clarabot_nano_common.utility.path.resolve import resolve_path
from ruamel import yaml
from tarka.utility.file import SafeFile

from clarabot_nano.common.request.constants import IDEMPOTENT_HANDLER_KEYS, HANDLER_KEY_PREFIX_MAP
from clarabot_nano.common.spawn.constants import ShortNames
from clarabot_nano.development.common import load_development_server_config
from clarabot_nano.development.spawn.common import write_spawn_debug_info
from clarabot_nano.development.spawn.server.document.dbg_handlers import (
    DebugInternalAuthInfoHandler,
    DebugStoreWopiTemplateHandler,
)
from clarabot_nano.development.spawn.server.general.dbg_handlers import DebugServerEchoCHandler
from clarabot_nano.gateway.job.inter import GatewayInterNanoJobQueue
from clarabot_nano.gateway.process import GatewayProcess
from clarabot_nano.gateway.state.gateway import GatewayState
from clarabot_nano.gateway.state.inter import GatewayInterNanoRequester
from clarabot_nano.inter.base import AbstractInterNanoExecutor

# allow testing multipart responses by extending the idempotent set
IDEMPOTENT_HANDLER_KEYS.add(DebugServerEchoCHandler.HANDLER_KEY)
IDEMPOTENT_HANDLER_KEYS.add(DebugInternalAuthInfoHandler.HANDLER_KEY)
IDEMPOTENT_HANDLER_KEYS.add(DebugStoreWopiTemplateHandler.HANDLER_KEY)
HANDLER_KEY_PREFIX_MAP[DebugInternalAuthInfoHandler.HANDLER_KEY] = ShortNames.DOCUMENT_SERVER
HANDLER_KEY_PREFIX_MAP[DebugStoreWopiTemplateHandler.HANDLER_KEY] = ShortNames.DOCUMENT_SERVER

# monkey-patch inter-nano job queue processing to delay for testing cancellation for example
_old_gateway_inter_nano_job_queue_run_job = GatewayInterNanoJobQueue._run_job


async def _patched_gateway_inter_nano_job_queue_run_job(self, job):
    await asyncio.sleep(1.0)
    return await _old_gateway_inter_nano_job_queue_run_job(self, job)


GatewayInterNanoJobQueue._run_job = _patched_gateway_inter_nano_job_queue_run_job

# monkey-patch backup processing to emit different errors to verify error-handling
_old_inter_nano_requester_perform = GatewayInterNanoRequester.perform


class _BackupResponseErrorMocker:
    SERVER_INTERNAL_ERROR = "internal error"
    NONFATAL_ERRORS = [
        NANO_REQUEST_ERROR_HANDLER_RESET_REQUEST,  # reset
        NANO_REQUEST_ERROR_PART_RANGE,  # reset
        NANO_REQUEST_ERROR_HANDLER_TEMPORARY_FAILURE,  # retry
        NANO_REQUEST_ERROR_CONFLICT,  # retry (unknown)
    ]
    FATAL_ERRORS = [
        NANO_REQUEST_ERROR_TOO_MANY_REQUESTS,
        SERVER_INTERNAL_ERROR,
    ]
    SYNC_TRIGGER_TIME = 60 * 60 * 25

    MOCK = True

    def __init__(self, gws: GatewayState, sp: NanoResponseStreamProcessor, cb: Callable[[int, dict], None]):
        self.gws = gws
        self.sp = sp
        self.cb = cb
        self.extra_p = random.randint(1, 2)
        self.marks = set()

    def _notify_backup_job(self, delay: float):
        self.gws._loop.call_later(delay, self._trigger_notify_backup_job)

    def _trigger_notify_backup_job(self):
        for drive_path, (room_id, backups) in self.gws.extracted_config.get_drive_backups().items():
            for packed_backup_id in backups:
                sf = SafeFile(Path(drive_path, f"Nano/Backup/{packed_backup_id}/Status.yml"))
                status = yaml.YAML(typ="safe", pure=True).load(StringIO(sf.read().decode("utf-8")))
                if status.get("current_fail"):
                    status["current_fail"] = status["current_fail"] - 600
                if status.get("last_start"):
                    status["last_start"] = status["last_start"] - self.SYNC_TRIGGER_TIME
                if status.get("last_finish"):
                    status["last_finish"] = status["last_finish"] - self.SYNC_TRIGGER_TIME
                sio = StringIO()
                yaml.YAML(typ="safe", pure=True).dump(status, sio)
                sf.write(sio.getvalue().encode("utf-8"))
        self.gws._backup_job_queue.put_nowait({"type": "config-change"})

    def __call__(self, i: int, resp: dict):  # NOTE: invoked in threadpool
        if os.environ.get("CBNANO_TEST_MOCK_INTER_NANO_ERRORS", "").lower() in ("1", "true", "yes", "on"):
            payload = resp.get("response")
            if isinstance(payload, dict) and self.MOCK:
                error = None
                if "mac-next" in self.marks:
                    error = random.choice(self.FATAL_ERRORS)
                    self.gws._loop.call_soon_threadsafe(self._notify_backup_job, 0.25)  # start retry after
                elif self.sp.part == 0:  # not continuation
                    if resp.get("part") == 0 and "p0" not in self.marks:
                        error = random.choice(self.NONFATAL_ERRORS)
                        self.marks.add("p0")
                    elif resp.get("part") == self.extra_p and "pe" not in self.marks:
                        error = random.choice(self.NONFATAL_ERRORS)
                        self.marks.add("pe")
                    elif "fc" in payload and resp.get("part") >= 20 and "fc3" not in self.marks:
                        error = random.choice(self.NONFATAL_ERRORS)
                        self.marks.add("fc3")
                elif "MAC" in payload and "MACS" in payload and "mac-next" not in self.marks:
                    self.marks.add("mac-next")
                if error:
                    print("MOCKING_ERROR", error)
                    if error == self.SERVER_INTERNAL_ERROR:
                        self.sp.server_error = error
                    else:
                        self.sp.error = error
            resp2 = dict(resp)
            if "response" in resp2 and "fc" in resp2["response"]:
                resp2["response"] = dict(resp2["response"])
                resp2["response"]["fc"] = resp2["response"]["fc"][:5]
        self.cb(i, resp)


async def _mocker_inter_nano_requester_perform(
    self, pool: ThreadPoolExecutor, stream_processor: NanoResponseStreamProcessor
):
    if not isinstance(stream_processor.response_handler_cb, _BackupResponseErrorMocker):
        mocker = _BackupResponseErrorMocker(self.gws, stream_processor, stream_processor.response_handler_cb)
        stream_processor.response_handler_cb = mocker
    return await _old_inter_nano_requester_perform(self, pool, stream_processor)


GatewayInterNanoRequester.perform = _mocker_inter_nano_requester_perform
AbstractInterNanoExecutor.REQUEST_RETRY_DELAYS = [x / 1000.0 for x in AbstractInterNanoExecutor.REQUEST_RETRY_DELAYS]
NanoResponseError.DEBUG = True


async def startup(development_server_config):
    load_development_server_config(development_server_config)
    proc = GatewayProcess()
    config_dir = resolve_path(proc.get_config()["local"]["config_dir"], strict=True)
    write_spawn_debug_info(config_dir, os.getpid(), ShortNames.GATEWAY)
    await proc.run()


def main():
    CommonConfigConstants.DEVELOPMENT = True
    parser = argparse.ArgumentParser("Clarabot Nano - Test Gateway process")
    parser.add_argument("development_server_config")
    args = parser.parse_args()
    asyncio.run(startup(args.development_server_config))


if __name__ == "__main__":
    main()
