"""A set of global constants for FireWorks (Python code as a config file)."""

from __future__ import annotations

import importlib
import os
from typing import Any

import pymongo
from monty.design_patterns import singleton
from monty.serialization import dumpfn, loadfn

from fireworks.utilities.exceptions import FWConfigurationError

__author__ = "Anubhav Jain"
__copyright__ = "Copyright 2012, The Materials Project"
__maintainer__ = "Anubhav Jain"
__email__ = "ajain@lbl.gov"
__date__ = "Dec 12, 2012"

NEGATIVE_FWID_CTR = 0

# this is where load_object() looks for serialized objects
USER_PACKAGES = ["fireworks.user_objects", "fw_tutorials", "fireworks.features"]

# if you update a _fw_name, you can use this to record the change and maintain deserialization
FW_NAME_UPDATES = {
    "Transfer Task": "FileTransferTask",
    "Script Task": "ScriptTask",
    "Template Writer Task": "TemplateWriterTask",
    "Dupe Finder Exact": "DupeFinderExact",
}

YAML_STYLE = False  # controls whether YAML documents will be nested as braces or blocks (False = blocks)

FW_BLOCK_FORMAT = "%Y-%m-%d-%H-%M-%S-%f"  # date format for writing block directories in "rapid-fire" mode

FW_LOGGING_FORMAT = "%(asctime)s %(levelname)s %(message)s"  # format for loggers

QUEUE_RETRY_ATTEMPTS = 10  # number of attempts to re-try communicating with queue server in failures
QUEUE_UPDATE_INTERVAL = 5  # max interval (seconds) needed for queue to update after submitting a job
QUEUE_JOBNAME_MAXLEN = 20  # max length of the jobname for queue systems

SUBMIT_SCRIPT_NAME = "FW_submit.script"  # name of submit script

PRINT_FW_JSON = True
PRINT_FW_YAML = False

JSON_SCHEMA_VALIDATE = False
JSON_SCHEMA_VALIDATE_LIST = []

PING_TIME_SECS = 3600  # while Running a job, how often to ping back the server that we're still alive
RUN_EXPIRATION_SECS = PING_TIME_SECS * 4  # mark job as FIZZLED if not pinged in this time

MAINTAIN_INTERVAL = 120  # seconds between maintenance intervals when running infinite maintenance

RESERVATION_EXPIRATION_SECS = 60 * 60 * 24 * 14  # a job can stay in a queue this long before we
# cancel its reservation

WFLOCK_EXPIRATION_SECS = 60 * 5  # wait this long for a WFLock before expiring
WFLOCK_EXPIRATION_KILL = False  # kill WFLock on expiration (or give a warning)

RAPIDFIRE_SLEEP_SECS = 60  # seconds to sleep between rapidfire loops

LAUNCHPAD_LOC = None  # where to find the my_launchpad.yaml file
FWORKER_LOC = None  # where to find the my_fworker.yaml file
QUEUEADAPTER_LOC = None  # where to find the my_qadapter.yaml file

CONFIG_FILE_DIR = "."  # directory containing config files (if not individually set)

STREAM_LOGLEVEL = "INFO"  # the default streaming log level used for various loggers
ROCKET_STREAM_LOGLEVEL = "INFO"  # the streaming log level of the rocket.launcher logger

QSTAT_FREQUENCY = 50  # set this higher to avoid qstats, lower to always

ALWAYS_CREATE_NEW_BLOCK = False  # always create new block on queue launcher call

TEMPLATE_DIR = None  # default template dir for TemplateWriterTask

REMOVE_USELESS_DIRS = True  # deletes empty launch dir if _launch_dir set

DS_PASSWORD = b"1234"  # dummy password to access DataServer

STORE_PACKING_INFO = True  # automatically add job packing info to stored_data

PW_CHECK_NUM = 10  # number of entries that can be modified in single lpad command w/o password

TRACKER_LINES = 25  # number of lines to return in Tracker

SORT_FWS = ""  # sort equal priority FWs? "FILO" or "FIFO".

ENCODE_MONTY = True  # detect and use Monty-style as_dict()

DECODE_MONTY = True  # detect and use Monty-style from_dict() with @class and @module

EXCEPT_DETAILS_ON_RERUN = False  # add exception details to the spec when rerunning the FW

WEBSERVER_HOST = "127.0.0.1"  # default host on which the Flask web server runs

WEBSERVER_PORT = 5000  # default port on which the Flask web server runs

WEBSERVER_PERFWARNINGS = False  # enable performance-related warnings

# value of socketTimeoutMS when connection to mongoDB.  See pymongo official
# documentation http://api.mongodb.org/python/current/api/pymongo/mongo_client.html
MONGO_SOCKET_TIMEOUT_MS = 5 * 60 * 1000

# name of the collection that will be used to store information in case the size of
# a dynamically generated document exceeds the 16MB limit. Functionality disabled if None.
GRIDFS_FALLBACK_COLLECTION = "fw_gridfs"

# path to a database file to use with mongomock, do not use mongomock if None
MONGOMOCK_SERVERSTORE_FILE = None

# default mongoclient class
MongoClient = pymongo.MongoClient


def override_user_settings() -> None:
    module_dir = os.path.dirname(os.path.abspath(__file__))
    root_dir = os.path.dirname(module_dir)  # FW root dir

    config_paths = []

    test_paths = [os.getcwd(), os.path.join(os.path.expanduser("~"), ".fireworks"), root_dir]

    for p in test_paths:
        fp = os.path.join(p, "FW_config.yaml")
        if fp not in config_paths and os.path.exists(fp):
            config_paths.append(fp)

    if "FW_CONFIG_FILE" in os.environ and os.environ["FW_CONFIG_FILE"] not in config_paths:
        config_paths.append(os.environ["FW_CONFIG_FILE"])

    config_paths = config_paths or [os.path.join(os.path.expanduser("~"), ".fireworks", "FW_config.yaml")]

    if len(config_paths) > 1:
        print(f"Found many potential paths for FW_CONFIG_FILE: {config_paths}\nChoosing as default: {config_paths[0]}")

    if os.path.exists(config_paths[0]):
        overrides = loadfn(config_paths[0])
        if not isinstance(overrides, dict):
            raise FWConfigurationError(f"Invalid FW_config file, type must be dict but is {type(overrides)}")
        for key, v in overrides.items():
            if key == "ADD_USER_PACKAGES":
                USER_PACKAGES.extend(v)
            elif key == "ECHO_TEST":
                print(v)
            elif key not in globals():
                raise FWConfigurationError(f"Invalid FW_config file has unknown parameter: {key}")
            else:
                globals()[key] = v

    for k in ["LAUNCHPAD_LOC", "FWORKER_LOC", "QUEUEADAPTER_LOC"]:
        if globals().get(k, None) is None:
            fname = "my_qadapter.yaml" if k == "QUEUEADAPTER_LOC" else f"my_{k.split('_')[0].lower()}.yaml"
            m_paths = []
            if os.path.realpath(CONFIG_FILE_DIR) not in test_paths:
                test_paths.insert(0, CONFIG_FILE_DIR)
            for p in test_paths:
                fp = os.path.join(p, fname)
                if os.path.exists(fp) and fp not in m_paths:
                    m_paths.append(fp)

            if len(m_paths) > 1:
                print(f"Found many potential paths for {k}: {m_paths}\nChoosing as default: {m_paths[0]}")

            if len(m_paths) > 0:
                globals()[k] = m_paths[0]

    if "MONGOMOCK_SERVERSTORE_FILE" in os.environ:
        globals()["MONGOMOCK_SERVERSTORE_FILE"] = os.environ["MONGOMOCK_SERVERSTORE_FILE"]
    if globals()["MONGOMOCK_SERVERSTORE_FILE"]:
        try:
            mongomock_persistence = importlib.import_module("mongomock_persistence")
            mongomock_gridfs = importlib.import_module("mongomock.gridfs")
        except (ModuleNotFoundError, ImportError) as err:
            msg = (
                "\nTo use mongomock instead of mongodb the extra mongomock must"
                " be installed, for example like this:\npip install fireworks[mongomock]"
            )
            raise RuntimeError(msg) from err
        if not os.environ.get("MONGOMOCK_SERVERSTORE_FILE"):
            os.environ["MONGOMOCK_SERVERSTORE_FILE"] = globals()["MONGOMOCK_SERVERSTORE_FILE"]
        globals()["MongoClient"] = mongomock_persistence.MongoClient
        if globals()["GRIDFS_FALLBACK_COLLECTION"]:
            mongomock_gridfs.enable_gridfs_integration()


override_user_settings()


def config_to_dict() -> dict[str, Any]:
    return {k: v for k, v in globals().items() if k.upper() == k and k != "NEGATIVE_FWID_CTR"}


def write_config(path: str | None = None) -> None:
    if path is None:
        path = os.path.join(os.path.expanduser("~"), ".fireworks", "FW_config.yaml")
    dumpfn(config_to_dict(), path)


@singleton
class FWData:
    """This class stores data that a Firetask might want to access, e.g. to see the runtime params."""

    def __init__(self) -> None:
        self.MULTIPROCESSING = None  # default single process framework
        self.NODE_LIST = None  # the node list for sub jobs
        self.SUB_NPROCS = None  # the number of process of the sub job
        self.DATASERVER = None  # the shared object manager
        self.Running_IDs = None
