import datetime
import getpass
import multiprocessing
import os
import random
import string
from base64 import b64encode
from os import path

import orjson
from jsonschema import validate, Draft7Validator
from jsonschema.exceptions import ValidationError


class BuildConfiguration:
    def __init__(self):
        config = parse_config()

        self.sentry_dsn = config.get("sentry_dsn", None)
        self.sentry_dsn_js = config.get("sentry_dsn_js", None)
        self.open_telemetry_endpoint = config.get("open_telemetry_endpoint", None)
        self.development_mode = config.get("development_mode", True)
        self.automatic_migrations = config.get("automatic_migrations", True)
        # Kemono3 `BAN` URL prefix for cache purging.
        self.ban_url: str | list[str] | None = config.get('ban_url')
        self.enable_notifications = False
        self.cache_ttl_for_recent_posts = int(config.get("cache_ttl_for_recent_posts", 5*60-10))
        from src.utils.utils import decode_b64

        """ Configuration for the frontend server. """
        self.webserver = config.get("webserver", {})
        # Secret key used to encrypt sessions.
        self.webserver["secret"] = self.webserver.get(
            "secret", "".join(random.choice(string.ascii_letters) for _ in range(32))
        )
        # How many workers and threads should run at once?
        self.webserver["workers"] = self.webserver.get("workers", multiprocessing.cpu_count())
        self.webserver["threads"] = self.webserver.get("threads", 1)
        # If you've dealt with how the trust of forwarding IPs works upstream, flip this off.
        self.webserver["ip_security"] = self.webserver.get("ip_security", True)
        # header for user country iso, generater by DDOS-GUARD
        self.webserver["country_header_key"] = self.webserver.get("country_header_key", "DDG-Connecting-Country")
        # Set additional uWSGI options if you want. Overrides any of the other options.
        self.webserver["uwsgi_options"] = self.webserver.get("uwsgi_options", {})
        # The port the site will be served on.
        self.webserver["port"] = self.webserver.get("port", 6942)
        # Logging mode.
        self.webserver["logging"] = self.webserver.get("logging", "DEBUG" if self.development_mode else "ERROR")
        # The URL at which the site is publicly accessible.
        # NOTE: `site` at root of config acceptable for backwards compatibility.
        if config.get("site"):
            # Only set backwards-compatibility values if the value is actually there.
            # Otherwise, `get()`s will see a `None` value instead of a non-existent one and throw things off.
            self.webserver["site"] = config["site"]
        self.webserver["site"] = self.webserver.get("site", f"http://localhost:{self.webserver["port"]}")
        # The location of the resources that will be served.
        self.webserver["static_folder"] = self.webserver.get(
            "static_folder",
            "../client/dev/static" if self.development_mode else "../client/dist/static",
        )
        self.webserver["template_folder"] = self.webserver.get(
            "template_folder",
            "../client/dev/pages" if self.development_mode else "../client/dist/pages",
        )
        self.webserver["jinja_bytecode_cache_path"] = self.webserver.get("jinja_bytecode_cache_path")
        self.webserver["max_full_text_search_input_len"] = self.webserver.get("max_full_text_search_input_len", 500)
        self.webserver["table_sample_bernoulli_sample_size"] = float(
            self.webserver.get("table_sample_bernoulli_sample_size", 0.1)
        )
        self.webserver["extra_pages_to_load_on_posts"] = int(self.webserver.get("extra_pages_to_load_on_posts", 1))
        self.webserver["pages_in_popular"] = int(self.webserver.get("pages_in_popular", 10))
        self.webserver["earliest_date_for_popular"] = datetime.date.fromisoformat(
            self.webserver.get("earliest_date_for_popular", "2022-02-01")
        )
        self.webserver["use_redis_by_lock_default_on_queries"] = self.webserver.get(
            "use_redis_by_lock_default_on_queries", False
        )

        self.webserver["api"] = self.webserver.get("api", {})
        self.webserver["api"]["creators_location"] = self.webserver["api"].get("creators_location", "")
        # Interface preferences and customization options.
        self.webserver["ui"] = self.webserver.get("ui", {})
        # Use custom fileservers. By default, files will simply point to the root.
        # If you have multiple, this'll let you load balance!
        # Each value in the array may either be a string pointing to the address
        #   of each server or a two-element array containing an address and split
        #   percentage (out of 100)
        self.webserver["ui"]["config"] = self.webserver["ui"].get("config", {})
        self.webserver["ui"]["config"]["artists_or_creators"] = self.webserver["ui"]["config"].get(
            "artists_or_creators", "Creators"
        )

        self.webserver["ui"]["config"]["paysite_list"] = self.webserver["ui"]["config"].get("paysite_list", [])

        self.webserver["ui"]["sidebar"] = self.webserver["ui"].get("sidebar", {})
        self.webserver["ui"]["sidebar"]["disable_filehaus"] = self.webserver["ui"]["sidebar"].get(
            "disable_filehaus", False
        )
        self.webserver["ui"]["sidebar"]["disable_faq"] = self.webserver["ui"]["sidebar"].get("disable_faq", False)
        self.webserver["ui"]["sidebar"]["disable_dms"] = self.webserver["ui"]["sidebar"].get("disable_dms", False)

        self.webserver["ui"]["home"] = self.webserver["ui"].get("home", {})
        self.webserver["ui"]["home"]["logo_path"] = self.webserver["ui"]["home"].get("logo_path", None)
        self.webserver["ui"]["home"]["mascot_path"] = self.webserver["ui"]["home"].get("mascot_path", None)
        self.webserver["ui"]["home"]["welcome_credits"] = decode_b64(
            self.webserver["ui"]["home"].get("welcome_credits", None)
        )
        self.webserver["ui"]["home"]["home_background_image"] = self.webserver["ui"]["home"].get(
            "home_background_image", None
        )

        self.webserver["ui"]["home"]["announcements"] = self.webserver["ui"]["home"].get("announcements", [])
        for announcement in self.webserver["ui"]["home"]["announcements"]:
            announcement["date"] = datetime.datetime.fromisoformat(announcement["date"]).strftime("%B %d, %Y")

        self.webserver["ui"]["home"]["site_name"] = self.webserver["ui"]["home"].get("site_name") or "This website"

        self.webserver["ui"]["files_url_prepend"] = self.webserver["ui"].get("files_url_prepend") or {}
        self.webserver["ui"]["files_url_prepend"]["icons_base_url"] = (
                self.webserver["ui"]["files_url_prepend"].get("icons_base_url") or ""
        )
        self.webserver["ui"]["files_url_prepend"]["banners_base_url"] = (
                self.webserver["ui"]["files_url_prepend"].get("banners_base_url") or ""
        )
        self.webserver["ui"]["files_url_prepend"]["thumbnails_base_url"] = (
                self.webserver["ui"]["files_url_prepend"].get("thumbnails_base_url") or ""
        )

        self.webserver["ui"]["fileservers"] = self.webserver["ui"].get("fileservers", None)
        # Add custom links to the bottom of the sidebar.
        # See `client/src/pages/components/shell.html` for an idea of what the format is like.
        self.webserver["ui"]["sidebar_items"] = self.webserver["ui"].get("sidebar_items", [])
        # Add custom HTML elements to the footer.
        self.webserver["ui"]["footer_items"] = self.webserver["ui"].get("footer_items", [])
        # Banner HTML. Each entry should be Base64-encoded.
        self.webserver["ui"]["banner"] = self.webserver["ui"].get("banner", {})
        self.webserver["ui"]["banner"]["global"] = decode_b64(self.webserver["ui"]["banner"].get("global", None))
        self.webserver["ui"]["banner"]["welcome"] = decode_b64(self.webserver["ui"]["banner"].get("welcome", None))
        # Ads preferences. Each spot should be Base64-encoded.
        self.webserver["ui"]["ads"] = self.webserver["ui"].get("ads", {})
        self.webserver["ui"]["ads"]["header"] = decode_b64(self.webserver["ui"]["ads"].get("header", None))
        self.webserver["ui"]["ads"]["middle"] = decode_b64(self.webserver["ui"]["ads"].get("middle", None))
        self.webserver["ui"]["ads"]["footer"] = decode_b64(self.webserver["ui"]["ads"].get("footer", None))
        self.webserver["ui"]["ads"]["slider"] = decode_b64(self.webserver["ui"]["ads"].get("slider", None))

        # This value should be a (still Base64-encoded!) JSON list of objects.
        # See https://docs.fluidplayer.com/docs/configuration/ads/#adlist
        self.webserver["ui"]["ads"]["video"] = decode_b64(
            self.webserver["ui"]["ads"].get("video", b64encode(orjson.dumps([])).decode())
        )
        # Matomo preferences.
        self.webserver["ui"]["matomo"] = self.webserver["ui"].get("matomo", {})
        self.webserver["ui"]["matomo"]["enabled"] = self.webserver["ui"]["matomo"].get("enabled", False)
        # Fill in the information based on the embed code you get from the panel;
        """
            [...]
            (function() {
              var u="//{{   tracking_domain   }}/";
              _paq.push(['setTrackerUrl', u+'{{   tracking_code   }}']);
              _paq.push(['setSiteId', '{{   site_id   }}']);
              var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
              g.async=true; g.src=u+'{{   tracking_code   }}.js'; s.parentNode.insertBefore(g,s);
            })();
        """
        self.webserver["ui"]["matomo"]["tracking_domain"] = self.webserver["ui"]["matomo"].get(
            "tracking_domain", "beta.kemono.party"
        )
        self.webserver["ui"]["matomo"]["tracking_code"] = self.webserver["ui"]["matomo"].get("tracking_code", "onomek")
        self.webserver["ui"]["matomo"]["site_id"] = self.webserver["ui"]["matomo"].get("site_id", 2)
        # ... or override the template by entering a plain Base64-encoded string.
        self.webserver["ui"]["matomo"]["plain_code"] = decode_b64(
            self.webserver["ui"]["matomo"].get("plain_code", None)
        )
        # File extensions recognized as (browser-friendly) video; will automatically be embedded in post pages.
        self.webserver["ui"]["video_extensions"] = self.webserver["ui"].get(
            "video_extensions", [".mp4", ".webm", ".m4v", ".3gp", ".mov"]
        )

        self.archive_server = config.get("archive_server", {})
        self.archive_server["api_url"] = self.archive_server.get("api_url")
        self.archive_server["serve_files"] = self.archive_server.get("serve_files", False)
        self.archive_server["enabled"] = self.archive_server.get("enabled", False)
        self.archive_server["file_serving_enabled"] = self.archive_server.get("file_serving_enabled", False)

        """ Filehaus configuration. """
        self.filehaus = config.get("filehaus", {})
        # If true, an account will be required for uploading.
        self.filehaus["requires_account"] = self.filehaus.get("requires_account", True)
        # Required account roles for uploading.
        # If set to an empty list, no permissions will be required.
        self.filehaus["required_roles"] = self.filehaus.get(
            "required_roles", ["administrator", "moderator", "uploader"]
        )
        # `tusd` configuration.
        self.filehaus["tus"] = self.filehaus.get("tus", {})
        # Automatically manage `tusd` on port 1080.
        # If you intend to store uploads on a different server from Kemono3,
        # set a custom URL instead...
        self.filehaus["tus"]["manage"] = self.filehaus["tus"].get("manage", False)
        # ...right here. Note that if you do not allow automatic management and did
        # not set a custom URL, uploads will be blackholed to a demo instance and
        # Filehaus sharing will not function correctly.
        # Do note that using `tusd` instances to enable Filehaus functionality requires
        # the `post-create` hook to be pointed at Kemono's `/shares/tus` endpoint.
        # `tusd --hooks-enabled-events post-create -hooks-http "http://127.0.0.1:6942/shares/tus" -upload-dir=./data`
        self.filehaus["tus"]["url"] = self.filehaus["tus"].get(
            "url",
            "http://localhost:1080" if self.filehaus["tus"]["manage"] else "https://tusd.tusdemo.net/files/",
        )

        """ Database configuration. """
        self.database = config.get("database", {})
        self.database["host"] = self.database.get("host", "127.0.0.1")
        self.database["port"] = self.database.get("port", 5432)
        self.database["password"] = self.database.get("password", "kemono")
        self.database["database"] = self.database.get("database", "kemono")
        self.database["user"] = self.database.get("user", "kemono")
        self.database["application_name"] = self.database.get("application_name", "Kemono")

        self.redis = config.get("redis", {})
        self.redis["compression"] = self.redis.get("compression", "zstd")
        self.redis["default_ttl"] = int(self.redis.get("default_ttl", "600"))
        self.redis["node_options"] = self.redis.get("defaults", {})
        self.redis["node_options"]["host"] = self.redis["node_options"].get("host", "127.0.0.1")
        self.redis["node_options"]["port"] = self.redis["node_options"].get("port", 6379)
        self.redis["node_options"]["db"] = self.redis["node_options"].get("db", 0)
        self.redis["node_options"]["password"] = self.redis["node_options"].get("password", None)
        self.redis["nodes"] = self.redis.get("nodes", {0: {"db": 0}})
        if isinstance(self.redis["nodes"], list):
            self.redis["nodes"] = dict((i, host) for i, host in enumerate(self.redis["nodes"]))
        else:
            self.redis["nodes"] = {int(k): v for k, v in self.redis["nodes"].items()}
        self.redis["keyspaces"] = self.redis.get("keyspaces", {})
        keyspaces = {
            "account",
            "saved_key_import_ids",
            "saved_keys",
            "top_artists",
            "random_artist_keys",
            "artist",
            "artist_post_count",
            "artists_by_update_time",
            "dms",
            "all_dms",
            "all_dms_count",
            "all_dms_by_query",
            "all_dms_by_query_count",
            "dms_count",
            "favorite_artists",
            "favorite_posts",
            "notifications_for_account",
            "random_post_keys",
            "post",
            "posts_incomplete_rewards",
            "comments",
            "posts_by_artist",
            "artist_posts_offset",
            "is_post_flagged",
            "importer_logs",
            "ratelimit",
            "all_posts",
            "all_shares",
            "all_shares_count",
            "all_posts_for_query",
            "global_post_count",
            "global_post_count_for_query",
            "lock",  # used in KemonoRedisLock
            "lock-signal",  # used in KemonoRedisLock
            "imports",
            "share_files",
            "account_notifications",
            "new_notifications",
            "share",
            "artist_shares",
            "post_revisions",
            "files",
            "post_by_id",
            "fancards",
            "announcements",
            "announcement_count",
            "artist_share_count",
            "discord_channels_for_server",
            "discord_posts",
            "popular_posts",
            "tagged_posts",
            "tags",
            "file",
            "archive_files",
            "linked_accounts",
        }

        for name in keyspaces:
            self.redis["keyspaces"][name] = self.redis["keyspaces"].get(name, 0)


global_config = None


def Configuration():
    global global_config
    if not global_config:
        global_config = BuildConfiguration()
    return global_config

# todo use watcher to reload config object based on json changes


def parse_config() -> dict:
    config_file_name = os.environ.get("KEMONO_CONFIG") or "config.json"
    override_config_file_name = os.environ.get("KEMONO_OVERRIDE_CONFIG") or "config.override.json"
    config_file_path = os.path.join("./", config_file_name)
    override_config_path = os.path.join("./", override_config_file_name)
    config_schema_file_path = path.join("./", "schema", "config.schema.json")
    config_schema = {}
    config = {}
    override_config = {}

    if not path.exists(config_schema_file_path):
        raise Exception(f'''Config schema file does not exist at path "{config_schema_file_path}".''')
    else:
        with open(config_schema_file_path) as f:
            config_schema.update(orjson.loads(f.read()))

    if not path.exists(config_file_path):
        raise Exception(f'''Config file does not exist at path \"{config_file_path}\".''')
    else:
        with open(config_file_path) as f:
            config.update(orjson.loads(f.read()))

    if path.exists(override_config_path):
        with open(override_config_path) as f:
            override_config.update(orjson.loads(f.read()))

    config = merge_dict(config, override_config)

    try:
        Draft7Validator.check_schema(config_schema)
        validate(instance=config, schema=config_schema, cls=Draft7Validator)
    except ValidationError as error:
        schema_json_path_segments = '/'.join([
            str(segment)
            for segment
            in error.absolute_schema_path
        ])
        schema_json_path = f"#/{schema_json_path_segments}"
        error_message = f"Configuration validation failed as per schema in \"{schema_json_path}\"."

        raise Exception(error_message)

    return config


def merge_dict(dict_base, dict_that_overrides):
    if isinstance(dict_base, dict) and isinstance(dict_that_overrides, dict):
        value = {
            **dict_base,
            **{
                k: merge_dict(dict_base.get(k, {}), v)
                for k, v
                in dict_that_overrides.items()
            }
        }

        return value
    else:
        return dict_that_overrides
