# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

# src/pdftl/registry_init.py

"""
Single entry point for initializing the application registry.
This function populates registry options and discovers all operations.
"""

import logging

import pdftl

logger = logging.getLogger(__name__)

import os
import pathlib
import sys


def _discover_external_operations():
    """Discover user-provided operations in the config directory."""
    import importlib

    if os.name == "nt":
        config_base = pathlib.Path(os.environ.get("APPDATA", ""))
    else:
        config_base = pathlib.Path.home() / ".config"

    op_dir = config_base / "pdftl" / "operations"

    if not op_dir.exists():
        return

    # Add op_dir to sys.path so the modules can be imported by name
    # and can find each other if they have local imports.
    op_path_str = str(op_dir)
    if op_path_str not in sys.path:
        sys.path.insert(0, op_path_str)  # Priority over built-ins if you want shadowing

    for py_file in op_dir.glob("*.py"):
        if py_file.stem == "__init__":
            continue

        module_name = py_file.stem
        try:
            # Using import_module because we added op_dir to sys.path
            importlib.import_module(module_name)
            logger.debug("Loaded external operation: %s", module_name)
        except ImportError as e:
            logger.error("Could not import external operation '%s': %s", module_name, e)
        except SyntaxError as e:
            logger.error(
                "Syntax error in external operation '%s' at line %s: %s",
                module_name,
                e.lineno,
                e.msg,
            )
        except Exception:
            # We catch bare Exception here to prevent a buggy user plugin
            # from stopping pdftl from starting entirely.
            logger.exception("Unexpected error loading external operation '%s'", module_name)


def _discover_modules(parent_modules, label):
    """
    Import submodules.

    This ensures that decorators are executed,
    so the global registry is fully populated before use.
    """
    import importlib
    import pkgutil

    loaded_modules = []
    for pkg in parent_modules:
        # iter_modules requires the path to be iterable (a list)
        path = getattr(pkg, "__path__", None)
        if path is None:
            logger.warning("Skipping discovery for %s (no __path__)", pkg.__name__)
            continue

        for _, module_name, _ in pkgutil.iter_modules(path):
            fq_name = f"{pkg.__name__}.{module_name}"
            importlib.import_module(fq_name)
            loaded_modules.append(fq_name)

    logger.debug("[registry_init] Loaded %s %s modules:", len(loaded_modules), label)
    for module in loaded_modules:
        logger.debug("  - %s", module)

    return loaded_modules


def initialize_registry():
    """
    Initialize the entire application registry.

    This function is idempotent (safe to call multiple times).
    It populates static options and discovers all operations.
    """
    import importlib

    if getattr(initialize_registry, "initialized", False):
        return

    # 1. Import the packages to be discovered
    # We must explicitly import 'utils' and 'cli' so their submodules (like
    # page_specs.py and pipeline.py) can be discovered and their decorators executed.
    for module in ["operations", "core", "output", "cli", "utils"]:
        importlib.import_module(f"pdftl.{module}")

    # 2. Discover and register all operations and help topics
    # We scan all relevant packages to find @register_operation and @register_help_topic
    _discover_modules([pdftl.operations, pdftl.core, pdftl.cli, pdftl.utils], "operation")
    _discover_modules([pdftl.output], "option")

    # 3. Discover external operations last (change order? depending on shadowing preference)
    _discover_external_operations()

    initialize_registry.initialized = True
