import os
import re
import glob
import click
import shutil
import tomlkit

from wurker.settings import conf
from wurker.models import Base
from wurker.cli.base import BaseCommand
from wurker.db import schema_exists, migrate


class StingerCommand(BaseCommand):
    """Encapsulate stinger commands."""

    def __init__(self, logger=None):
        BaseCommand.__init__(self, logger=logger)

        self.app_root = conf.app_root
        self.conf_root = conf.conf_root
        self.etc_root = conf.etc_root
        self.opt_root = conf.opt_root
        self.data_root = conf.data_root

    def _init_default_deps(self, root:str):
        """Init default dependencies in the etc root"""

        self.default_deps = {}
        default_root = os.path.join(self.etc_root, "stingers")
        if default_root != root:
            self.default_deps = self._get_deps(root=default_root, slug="default")

    def run(
            self,
            operation:str,
            root:str='',
            slug:str='',
            node:str='',
            env:str='',
            target:str='',
            resources:list=[],
            drop_schema:bool=False
    ):
        """Perform Stinger operations with dependency tree(s)."""

        root = os.path.abspath(os.path.expanduser(root))
        self._init_default_deps(root)

        if operation == "unsting":
            return self._delete_deps(drop_schema=drop_schema)

        elif operation == "extract":
            return self._extract_deps(target=target, root=root, slug=slug, node=node, env=env, resources=resources)

        elif operation == "resting":
            self._delete_deps(drop_schema=drop_schema)
            return self._inject_deps(root=root, slug=slug, node=node, env=env)

        elif operation == "sting":
            return self._inject_deps(root=root, slug=slug, node=node, env=env)

        elif operation == "refactor":
            return self._refactor_stingers(root=root)

        else:
            raise ValueError(f"Invalid 'stinger' operation: '{operation}'")

    def _get_deps(self, deps:dict={}, root:str='', slug:str='', node:str='', env:str=''):
        """Return a dependency tree for the given Stingers root and args."""

        if not os.path.exists(root):
            raise Exception(f"Stingers root not found: '{root}'")

        stinger_dir = os.path.join(root, slug)
        if not os.path.exists(stinger_dir):
            raise Exception(f"Stinger '{slug}' not found: '{stinger_dir}'")

        wurker_dir = os.path.join(stinger_dir, "wurker")
        colony_dir = os.path.join(stinger_dir, "colony")

        deps = deps or {
            "requirements.txt": [],
            "wurker.toml": [],
            "seeds": [],
            "models": [],
            "settings": [],
            "bees": [],
            "lib": [],
            "lib/mail": [],
            "ssl": [],
            "data": [],
        }

        click.echo("Processing dependency directories...")

        # Do Colony 1st... it overrides Wurker
        node_dir = None
        if os.path.exists(colony_dir):
            node_dir = os.path.join(colony_dir, node) if node else None
            if node_dir and os.path.exists(node_dir):
                env_dir = os.path.join(node_dir, env) if env else None
                if env_dir and os.path.exists(env_dir):
                    self._load_deps(env_dir, deps)

                # Nodes may have wurker dirs, also
                node_wurker_dir = os.path.join(node_dir, "wurker")
                if os.path.exists(node_wurker_dir):
                    self._load_deps(node_wurker_dir, deps)

        # Now do Wurker...
        if os.path.exists(wurker_dir):
            self._load_deps(wurker_dir, deps)

        return deps

    def _load_deps(self, dep_dir:str, deps:dict):
        """Load dependency paths into provided dict of lists."""

        if not deps:
            return

        click.echo(f"  . Processing dependency directory: '{dep_dir}'...")

        for dep in [d for d,p in deps.items()]:
            path = os.path.join(dep_dir, dep)
            if os.path.exists(path):
                if os.path.isfile(path) or os.listdir(path):
                    deps[dep].append(os.path.join(dep_dir, dep))

    def _inject_deps(self, root:str, slug:str, node:str, env:str):
        """Now inject deps."""

        deps = self._get_deps(root=root, slug=slug, node=node, env=env, deps=self.default_deps)
        if not deps:
            return

        click.echo(f"Injecting dependencies...")

        dst_roots = {
            "requirements.txt": self.etc_root,
            "wurker.toml": self.etc_root,
            "seeds": self.etc_root,
            "models": self.opt_root,
            "settings": self.opt_root,
            "bees": self.opt_root,
            "lib": self.opt_root,
            "lib/mail": self.opt_root,
            "ssl": os.path.join(self.etc_root, "nginx"),
            "data": self.conf_root,
        }

        for dep, src_list in deps.items():
            if not src_list:
                continue

            dst = os.path.join(dst_roots[dep], dep)

            # Single file? Should only be one if Stingers set up correctly, but we pop, anyway
            if dep in ["requirements.txt", "wurker.toml",] and len(src_list):
                src = src_list.pop()

                if os.path.isfile(src):
                    click.echo(f"  . Injecting: '{src}' -> '{dst}'")
                    shutil.copyfile(src, dst)

                continue

            # Directories? Walk them and copy over missing dirs & files
            for src in src_list:
                for src_dir, dirs, files in os.walk(src):
                    dst_dir = src_dir.replace(src, dst, 1)

                    if not os.path.exists(dst_dir):
                        click.echo(f"  . Creating directory: '{dst_dir}'")
                        os.makedirs(dst_dir)

                    for f in files:
                        src_file = os.path.join(src_dir, f)
                        dst_file = os.path.join(dst_dir, f)
                        if os.path.exists(dst_file):
                            # Seed 000 should always get overwritten
                            if os.path.basename(dst_file) == "000_seed.json":
                                click.echo(f"  . Overwriting existing '{dst_file}'")
                                os.remove(dst_file)
                            else:
                                click.echo(f"  . Skipping existing '{dst_file}'")
                                continue

                        click.echo(f"  . Injecting: '{src_file}' -> '{dst_dir}'")
                        shutil.copy(src_file, dst_dir)

        # Make sure config file present
        if not os.path.exists(os.path.join(self.etc_root, "wurker.toml")):
            raise Exception("File not found: 'wurker.toml'")

    def _delete_deps(self, drop_schema=False):
        """Unseed and delete all the injected dependencies."""

        app_id = conf.app_id
        if not app_id:
            raise Exception("Missing valid WURKER_APP_ID")

        if drop_schema and schema_exists(self.remote_db_conf, Base, return_bool=True):
            click.echo(f"Dropping schema '{self.remote_db_conf.database}'...")

            if not migrate(self.remote_db_conf, "downgrade"):
                raise Exception(f"Unable to drop schema '{self.remote_db_conf.database}'")

            # Finally delete the local DB file
            if os.path.isfile(self.local_db_conf.database):
                click.echo(f"Deleting local database file: '{self.local_db_conf.database}'")
                os.remove(self.local_db_conf.database)

        dst_roots = {
            "wurker.db": self.data_root,
            "requirements.txt": self.etc_root,
            "wurker.toml": self.etc_root,
            "seeds": self.etc_root,
            "models": self.opt_root,
            "settings": self.opt_root,
            "bees": self.opt_root,
            "lib": self.opt_root,
            "lib/mail": self.opt_root,
            "ssl": os.path.join(self.etc_root, "nginx"),
            "data": self.conf_root,
        }

        # Some files to NOT delete
        ignore = [
            "__init__.py", "wurker.py", "dispatch.py", "test_pybee.py", "dispatchers", "__pycache__",
            "base.py", "templates", "example.html", "example.txt", "wurker.db",
        ]

        click.echo(f"Deleting dependencies...")

        for dep, dst_root in dst_roots.items():
            dst = os.path.join(dst_root, dep)

            # Single file? Then just delete it
            if dep in ["requirements.txt"]:
                if os.path.exists(dst):
                    click.echo(f"  . Deleting file: '{dst}'")
                    os.remove(dst)

                continue

            # Directories? Then it really depends
            if dep == "seeds" and os.path.exists(dst):
                click.echo(f"  . Deleting seeds: '{dst}'")
                shutil.rmtree(dst)

            elif dep == "models":
                for model in [f for f in glob.glob(os.path.join(dst, "*.py")) if os.path.basename(f) not in ignore]:
                    click.echo(f"  . Deleting model: '{model}'")
                    os.remove(model)

            elif dep == "settings":
                for setting in [f for f in glob.glob(os.path.join(dst, "*.py")) if os.path.basename(f) not in ignore]:
                    click.echo(f"  . Deleting setting: '{setting}'")
                    os.remove(setting)

                dst = os.path.join(dst, "bees")
                for setting in [f for f in glob.glob(os.path.join(dst, "*.py")) if os.path.basename(f) not in ignore]:
                    click.echo(f"  . Deleting bee setting: '{setting}'")
                    os.remove(setting)

            elif dep == "bees":
                for bee in [f for f in glob.glob(os.path.join(dst, "*.py")) if os.path.basename(f) not in ignore]:
                    click.echo(f"  . Deleting bee: '{bee}'")
                    os.remove(bee)

                # Also delete any custom submodules, except dispatchers/
                dirs = [
                    f for f in glob.glob(os.path.join(dst, "*"))
                    if os.path.isdir(f) and os.path.basename(f) not in ignore
                ]
                for d in dirs:
                    click.echo(f"  . Deleting bee directory: '{d}'")
                    shutil.rmtree(d)

                dst = os.path.join(dst, "dispatchers")
                for cb in [f for f in glob.glob(os.path.join(dst, "*.py")) if os.path.basename(f) not in ignore]:
                    click.echo(f"  . Deleting dispatcher bee: '{cb}'")
                    os.remove(cb)

            elif dep == "lib":
                for lib in [f for f in glob.glob(os.path.join(dst, "*.py")) if os.path.basename(f) not in ignore]:
                    click.echo(f"  . Deleting lib: '{lib}'")
                    os.remove(lib)

            elif dep == "lib/mail":
                for item in [f for f in glob.glob(os.path.join(dst, "*.py")) if os.path.basename(f) not in ignore]:
                    click.echo(f"  . Deleting mail client: '{item}'")
                    os.remove(item)

                # Also delete injected templates
                dst = os.path.join(dst, "templates")
                for item in [f for f in glob.glob(os.path.join(dst, "*")) if os.path.basename(f) not in ignore]:
                    if os.path.isdir(item):
                        click.echo(f"  . Deleting mail template directory: '{item}'")
                        shutil.rmtree(item)
                    else:
                        click.echo(f"  . Deleting mail template: '{item}'")
                        os.remove(item)

            elif dep == "data":
                for data in [f for f in glob.glob(os.path.join(dst, "*")) if os.path.basename(f) not in ignore]:
                    if os.path.isdir(data):
                        click.echo(f"  . Deleting data directory: '{data}'")
                        shutil.rmtree(data)
                    else:
                        click.echo(f"  . Deleting data file: '{data}'")
                        os.remove(data)

    def _extract_deps(self, target:str, resources:list=[], root:str='', slug:str='', node:str='', env:str=''):
        """Extract (copy) a Stinger out to target directory ('wurker' or 'colony')."""

        if target not in ['wurker', 'colony']:
            raise Exception("Extraction target required: 'wurker' or 'colony'")

        if not root:
            raise Exception("Missing required --extract-root path")

        src_roots = {
            "requirements.txt": self.etc_root,
            "wurker.toml": self.etc_root,
            "seeds": self.etc_root,
            "models": self.opt_root,
            "settings": self.opt_root,
            "settings/bees": self.opt_root,
            "bees": self.opt_root,
            "lib": self.opt_root,
            "lib/mail": self.opt_root,
            "ssl": os.path.join(self.etc_root, "nginx"),
            "data": self.data_root,
        }

        # Build up the destination roots based on args and target
        target_root = os.path.join(root, slug, target)
        if target == "colony":
            target_root = os.path.join(target_root, node, env)

        # All deps?
        deps = resources
        if not resources or resources[0].lower() == "all":
            deps = list(src_roots.keys())

        dst_roots = {d:target_root for d in src_roots if d in deps}

        # Some files to NOT extract
        ignore = [
            "__init__.py", "wurker.py", "dispatch.py", "test_pybee.py", "000_seed.json", "wurker.db",
            "base.py", "example.html", "example.txt",
        ]

        for dep, dst_root in dst_roots.items():
            if not os.path.exists(dst_root):
                os.makedirs(dst_root, exist_ok=True)

            src = os.path.join(src_roots[dep], dep)
            dst = os.path.join(dst_root, dep)

            # Single file? Then just copy it
            if dep in ["requirements.txt", "wurker.toml"]:
                if not os.path.exists(src):
                    click.echo(f"  . File not found: '{src}'")
                    continue

                if os.path.exists(dst):
                    click.echo(f"  . Overwriting file: '{dst}'")
                    os.remove(dst)

                click.echo(f"  . Extracting file: '{src}' -> '{dst}'")
                shutil.copy(src, dst)

                continue

            # Directories? Then it really depends
            if not os.path.exists(dst):
                os.makedirs(dst, exist_ok=True)

            if not os.path.exists(src):
                click.echo(f"  . Directory not found: '{src}'")
                continue

            if dep == "seeds":
                for seed in [f for f in glob.glob(os.path.join(src, "*.json")) if os.path.basename(f) not in ignore]:
                    click.echo(f"  . Extracting seed: '{seed}' -> '{dst}'")
                    shutil.copy(seed, dst)

            elif dep == "models":
                for model in [f for f in glob.glob(os.path.join(src, "*.py")) if os.path.basename(f) not in ignore]:
                    click.echo(f"  . Extracting model: '{model}' -> '{dst}'")
                    shutil.copy(model, dst)

            elif dep == "settings":
                for setting in [f for f in glob.glob(os.path.join(src, "*.py")) if os.path.basename(f) not in ignore]:
                    click.echo(f"  . Extracting setting: '{setting}' -> '{dst}'")
                    shutil.copy(setting, dst)

            elif dep == "settings/bees":
                for setting in [f for f in glob.glob(os.path.join(src, "*.py")) if os.path.basename(f) not in ignore]:
                    click.echo(f"  . Extracting bee setting: '{setting}' -> '{dst}'")
                    shutil.copy(setting, dst)

            elif dep == "bees":
                for bee in [f for f in glob.glob(os.path.join(src, "*.py")) if os.path.basename(f) not in ignore]:
                    click.echo(f"  . Extracting bee: '{bee}' -> '{dst}'")
                    shutil.copy(bee, dst)

                src = os.path.join(src, "dispatchers")
                dst = os.path.join(dst, "dispatchers")
                if not os.path.exists(dst):
                    os.makedirs(dst, exist_ok=True)

                for cb in [f for f in glob.glob(os.path.join(src, "*.py")) if os.path.basename(f) not in ignore]:
                    click.echo(f"  . Extracting dispatcher bee: '{cb}' -> '{dst}'")
                    shutil.copy(cb, dst)

            elif dep == "ssl":
                for cert in [f for f in glob.glob(os.path.join(src, "*.*")) if os.path.basename(f) not in ignore]:
                    click.echo(f"  . Extracting SSL certificates: '{cert}' => '{dst}'")
                    shutil.copy(cert, dst)

            elif dep == "lib":
                for lib in [f for f in glob.glob(os.path.join(src, "*.py")) if os.path.basename(f) not in ignore]:
                    click.echo(f"  . Extracting lib: '{lib}' -> '{dst}'")
                    shutil.copy(lib, dst)

            elif dep == "lib/mail":
                for item in [f for f in glob.glob(os.path.join(src, "*.py")) if os.path.basename(f) not in ignore]:
                    click.echo(f"  . Extracting mail client: '{item}' -> '{dst}'")
                    shutil.copy(item, dst)

                src = os.path.join(src, "templates")
                dst = os.path.join(dst, "templates")
                if not os.path.exists(dst):
                    os.makedirs(dst, exist_ok=True)

                for item in [f for f in glob.glob(os.path.join(src, "*")) if os.path.basename(f) not in ignore]:
                    click.echo(f"  . Extracting mail template: '{item}' -> '{dst}'")

                    if os.path.isdir(item):
                        dst = os.path.join(dst, os.path.basename(item))
                        shutil.copytree(item, dst, dirs_exist_ok=True)
                    else:
                        shutil.copy(item, dst)

            elif dep == "data":
                for data in [f for f in glob.glob(os.path.join(src, "*")) if os.path.basename(f) not in ignore]:
                    click.echo(f"  . Extracting data: '{data}' -> '{dst}'")
                    shutil.copy(data, dst)


    def _refactor_stingers(self, root:str):
        """Refactor legacy stingers, moving seeds and replacing .env with wurker.toml"""

        # 1st refactor seed filenames
        self._refactor_seed_filenames(root)

        # Then refactor .env to wurker.toml
        self._refactor_env_to_toml(root)


    def _refactor_seed_filenames(self, root:str):
        """
        Walks through the directory tree starting at root. For any directory named 'seeds',
        renames files matching the pattern 'seed_data_XXX.json' to 'XXX_seed.json',
        where XXX is a three-digit number.
        """

        # Compile a regular expression that captures the three digits.
        pattern = re.compile(r"^seed_data_(\d{3})\.json$")

        # os.walk walks the directory tree
        for dirpath, dirnames, filenames in os.walk(root):
            # Check if the current directory is named "seeds"
            if os.path.basename(dirpath) == "seeds":
                for filename in filenames:
                    match = pattern.match(filename)
                    if match:
                        three_digits = match.group(1)
                        new_filename = f"{three_digits}_seed.json"
                        old_path = os.path.join(dirpath, filename)
                        new_path = os.path.join(dirpath, new_filename)
                        click.echo(f"Renaming '{old_path}' to '{new_path}'")
                        try:
                            os.rename(old_path, new_path)
                        except Exception as e:
                            click.echo(f"Error renaming file {old_path}: {e}")


    def _refactor_env_to_toml(self, root:str):
        """Walks the directory tree starting at root, and converts '.env' to 'wurker.toml' config files."""

        # Helper: Convert a string that should represent a boolean.
        def str_to_bool(s):
            if isinstance(s, str):
                s = s.strip().lower()
                if s == "true":
                    return True
                if s == "false":
                    return False
            return s

        def parse_env_file(filepath):
            """Parse a legacy .env file into a dictionary, ignoring comments and blanks."""
            env_data = {}
            with open(filepath, "r") as f:
                for line in f:
                    # Strip whitespace.
                    line = line.strip()
                    # Skip empty lines and comments.
                    if not line or line.startswith("#"):
                        continue
                    # Split on first '='.
                    if "=" in line:
                        key, value = line.split("=", 1)
                        env_data[key.strip()] = value.strip()
            return env_data

        def convert_env_to_toml(env_data):
            """Convert parsed .env dictionary into a dictionary structure for the new TOML file."""

            # Build the [wurker] section
            wurker_section = {
                "name": env_data.get("WURKER_NAME", ""),
                "app_id": env_data.get("WURKER_APP_ID", ""),
                "env": env_data.get("WURKER_ENV", ""),
                "log": env_data.get("WURKER_LOG", ""),
                "loglevel": env_data.get("WURKER_LOGLEVEL", ""),
                "is_queen": str_to_bool(env_data.get("WURKER_IS_QUEEN", "false")),
                "debug": str_to_bool(env_data.get("WURKER_DEBUG", "false"))
            }

            # Build the [ui] section.
            # 'domain' comes from WURKER_SERVER_DOMAIN.
            ui_section = {
                "domain": env_data.get("WURKER_SERVER_DOMAIN", ""),
                "workers": 3
            }

            # [local_db] is fixed:
            local_db_section = {
                "hostname": "",
                "username": "",
                "password": "",
                "database": "wurker.db",
                "dbdriver": "sqlite",
                "port": 0
            }

            # Build the [remote_db] section.
            remote_db_section = {
                "hostname": env_data.get("WURKER_DB_HOSTNAME", ""),
                "username": env_data.get("WURKER_DB_USERNAME", ""),
                "password": env_data.get("WURKER_DB_PASSWORD", ""),
                "database": env_data.get("WURKER_DB_DATABASE", ""),
                "dbdriver": env_data.get("WURKER_DB_DRIVER", ""),
                "port": int(env_data.get("WURKER_DB_PORT", 0))
            }

            # Construct final configuration dictionary
            config = {
                "wurker": wurker_section,
                "ui": ui_section,
                "local_db": local_db_section,
                "remote_db": remote_db_section
            }

            return config

        def write_toml_file(config, output_path):
            doc = tomlkit.document()
            for section, data in config.items():
                doc[section] = data
            with open(output_path, "w") as f:
                f.write(tomlkit.dumps(doc))

        def process_env_file(env_filepath):
            click.echo(f"Processing legacy env file: {env_filepath}")
            # Parse the .env file.
            env_data = parse_env_file(env_filepath)

            # Convert to new configuration dictionary.
            config = convert_env_to_toml(env_data)

            # Determine output path: write wurker.toml in the same directory as the .env file.
            dirpath = os.path.dirname(env_filepath)
            output_path = os.path.join(dirpath, "wurker.toml")

            # Write out the TOML file.
            write_toml_file(config, output_path)
            click.echo(f"Wrote new configuration to: {output_path}")

            # Optionally delete the old .env file.
            os.remove(env_filepath)
            click.echo(f"Deleted legacy env file: {env_filepath}")

        # Recursively find and process all legacy .env files in the given root.
        for dirpath, dirnames, filenames in os.walk(root):
            for filename in filenames:
                if filename == ".env":
                    env_filepath = os.path.join(dirpath, filename)
                    process_env_file(env_filepath)
