<?php
/**
 * Module registry and loader for Noesia Smart Waiter.
 *
 * @package Noesia_Smart_Waiter
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

require_once dirname( __FILE__ ) . '/interface-noesia-swa-module.php';

if ( ! class_exists( 'Noesia_SWA_Modules' ) ) {
    /**
     * Handles discovery, registration, and bootstrapping of free/pro modules.
     */
    final class Noesia_SWA_Modules {

        /**
         * Free module tier slug.
         */
        const TIER_FREE = 'free';

        /**
         * Pro module tier slug.
         */
        const TIER_PRO = 'pro';

        /**
         * Absolute plugin root directory.
         *
         * @var string
         */
        private $plugin_dir = '';

        /**
         * Registered discovery paths.
         *
         * @var array<string, array<int, string>>
         */
        private $paths = array();

        /**
         * Registered module instances.
         *
         * @var array<string, Noesia_SWA_Module_Interface>
         */
        private $modules = array();

        /**
         * Track whether boot has already run.
         *
         * @var bool
         */
        private $booted = false;

        /**
         * Constructor.
         *
         * @param string $plugin_file Main plugin file path.
         */
        public function __construct( $plugin_file ) {
            $plugin_file      = (string) $plugin_file;
            $this->plugin_dir = trailingslashit( dirname( $plugin_file ) );

            $this->paths = array(
                self::TIER_FREE => array(),
                self::TIER_PRO  => array(),
            );

            $this->register_default_paths();
        }

        /**
         * Register the default free/pro module directories.
         *
         * @return void
         */
        private function register_default_paths() {
            $this->add_path( self::TIER_FREE, $this->plugin_dir . 'modules/free/' );
            $this->add_path( self::TIER_PRO, $this->plugin_dir . 'pro/modules/' );
        }

        /**
         * Add a discovery path for a tier.
         *
         * @param string $tier Tier slug.
         * @param string $path Absolute path.
         *
         * @return bool
         */
        public function add_path( $tier, $path ) {
            $tier = $this->normalize_tier( $tier );
            $path = trailingslashit( (string) $path );

            if ( '' === $tier || '' === $path ) {
                return false;
            }

            if ( ! in_array( $path, $this->paths[ $tier ], true ) ) {
                $this->paths[ $tier ][] = $path;
            }

            return true;
        }

        /**
         * Get all registered raw discovery paths.
         *
         * @param string $tier Optional tier filter.
         *
         * @return array
         */
        public function get_paths( $tier = '' ) {
            $tier = $this->normalize_tier( $tier );

            if ( $tier && isset( $this->paths[ $tier ] ) ) {
                return $this->paths[ $tier ];
            }

            return $this->paths;
        }

        /**
         * Get discovery paths after filters and runtime gating.
         *
         * This is the public contract other shell services should use when they
         * need to inspect module roots. It keeps admin/public asset discovery in
         * sync with the real module loader instead of re-hardcoding free/pro
         * folders elsewhere in the kernel.
         *
         * @param string $tier Optional tier filter.
         *
         * @return array
         */
        public function get_discovery_paths( $tier = '' ) {
            $paths = $this->get_filtered_paths();
            $tier  = $this->normalize_tier( $tier );

            if ( '' !== $tier ) {
                return isset( $paths[ $tier ] ) ? $paths[ $tier ] : array();
            }

            return $paths;
        }

        /**
         * Discover and boot all enabled modules.
         *
         * Safe to call more than once; repeated calls are ignored.
         *
         * @return void
         */
        public function boot() {
            if ( $this->booted ) {
                return;
            }

            $this->booted = true;

            /**
             * Allow external code to register paths before discovery.
             *
             * @param Noesia_SWA_Modules $this Module manager instance.
             */
            do_action( 'noesia_swa_modules_before_discover', $this );

            $this->discover_modules();

            /**
             * Allow external/manual registrations after file discovery.
             *
             * @param Noesia_SWA_Modules $this Module manager instance.
             */
            do_action( 'noesia_swa_register_modules', $this );

            foreach ( $this->modules as $module ) {
                if ( $module->is_enabled() ) {
                    $module->register_hooks();
                }
            }

            /**
             * Fires after all enabled modules register hooks.
             *
             * @param Noesia_SWA_Modules $this Module manager instance.
             */
            do_action( 'noesia_swa_modules_loaded', $this );
        }

        /**
         * Register a module instance manually.
         *
         * @param Noesia_SWA_Module_Interface $module Module instance.
         *
         * @return bool
         */
        public function register_module( Noesia_SWA_Module_Interface $module ) {
            $module_id = sanitize_key( $module->get_id() );

            if ( '' === $module_id ) {
                return false;
            }

            if ( isset( $this->modules[ $module_id ] ) ) {
                return false;
            }

            $tier = $this->normalize_tier( $module->get_tier() );

            if ( '' === $tier ) {
                return false;
            }

            $this->modules[ $module_id ] = $module;

            /**
             * Fires after a module instance is registered.
             *
             * @param string                      $module_id Module slug.
             * @param Noesia_SWA_Module_Interface $module    Module instance.
             * @param Noesia_SWA_Modules          $this      Module manager instance.
             */
            do_action( 'noesia_swa_module_registered', $module_id, $module, $this );

            return true;
        }

        /**
         * Get all registered modules.
         *
         * @param string $tier Optional tier filter.
         *
         * @return array<string, Noesia_SWA_Module_Interface>
         */
        public function get_modules( $tier = '' ) {
            $tier = $this->normalize_tier( $tier );

            if ( '' === $tier ) {
                return $this->modules;
            }

            $filtered = array();

            foreach ( $this->modules as $module_id => $module ) {
                if ( $tier === $this->normalize_tier( $module->get_tier() ) ) {
                    $filtered[ $module_id ] = $module;
                }
            }

            return $filtered;
        }

        /**
         * Get one registered module by ID.
         *
         * @param string $module_id Module slug.
         *
         * @return Noesia_SWA_Module_Interface|null
         */
        public function get_module( $module_id ) {
            $module_id = sanitize_key( (string) $module_id );

            if ( isset( $this->modules[ $module_id ] ) ) {
                return $this->modules[ $module_id ];
            }

            return null;
        }

        /**
         * Whether a module is registered.
         *
         * @param string $module_id Module slug.
         *
         * @return bool
         */
        public function has_module( $module_id ) {
            return null !== $this->get_module( $module_id );
        }

        /**
         * Get candidate entry files for a module slug and tier.
         *
         * Supports both flat and self-contained folder structures.
         *
         * @param string $module_id Module slug.
         * @param string $tier      Tier slug.
         *
         * @return array<int,string>
         */
        public function get_module_entry_candidates( $module_id, $tier = '' ) {
            $module_id = sanitize_key( (string) $module_id );
            $tier      = $this->normalize_tier( $tier );
            $paths     = $this->get_discovery_paths( $tier );
            $files     = array();

            if ( '' === $module_id || empty( $paths ) || ! is_array( $paths ) ) {
                return $files;
            }

            foreach ( $paths as $path ) {
                $path = trailingslashit( (string) $path );

                if ( '' === $path ) {
                    continue;
                }

                $folder_entry = $path . $module_id . '/class-noesia-swa-module-' . $module_id . '.php';
                $flat_entry   = $path . 'class-noesia-swa-module-' . $module_id . '.php';

                if ( file_exists( $folder_entry ) ) {
                    $files[] = $folder_entry;
                }

                if ( file_exists( $flat_entry ) ) {
                    $files[] = $flat_entry;
                }
            }

            return array_values( array_unique( $files ) );
        }

        /**
         * Resolve the preferred entry file for a module slug and tier.
         *
         * Folder-based modules are preferred over flat modules.
         *
         * @param string $module_id Module slug.
         * @param string $tier      Tier slug.
         *
         * @return string
         */
        public function get_module_entry_file( $module_id, $tier = '' ) {
            $files = $this->get_module_entry_candidates( $module_id, $tier );

            if ( empty( $files ) ) {
                return '';
            }

            foreach ( $files as $file ) {
                if ( false !== strpos( $file, '/' . sanitize_key( (string) $module_id ) . '/class-noesia-swa-module-' ) ) {
                    return $file;
                }
            }

            return isset( $files[0] ) ? (string) $files[0] : '';
        }

        /**
         * Whether a module entry file exists for a tier.
         *
         * @param string $module_id Module slug.
         * @param string $tier      Tier slug.
         *
         * @return bool
         */
        public function has_module_entry_file( $module_id, $tier = '' ) {
            return '' !== $this->get_module_entry_file( $module_id, $tier );
        }

        /**
         * Check whether the plugin can discover/load pro modules.
         *
         * @return bool
         */
        public function pro_available() {
            $pro_loader_file = $this->plugin_dir . 'pro/class-noesia-swa-pro-loader.php';
            $pro_module_dir  = $this->plugin_dir . 'pro/modules/';

            if ( file_exists( $pro_loader_file ) ) {
                return true;
            }

            return is_dir( $pro_module_dir );
        }

        /**
         * Discover module files from configured directories.
         *
         * @return void
         */
        private function discover_modules() {
            $paths = $this->get_filtered_paths();

            foreach ( $paths as $tier => $tier_paths ) {
                foreach ( $tier_paths as $path ) {
                    $this->discover_modules_in_path( $path );
                }
            }
        }

        /**
         * Get the active discovery paths after filters and gating.
         *
         * @return array<string,array<int,string>>
         */
        private function get_filtered_paths() {
            $paths = $this->get_paths();

            /**
             * Filter module discovery paths.
             *
             * @param array              $paths   Tiered discovery paths.
             * @param Noesia_SWA_Modules $this    Module manager instance.
             */
            $paths = apply_filters( 'noesia_swa_module_paths', $paths, $this );

            if ( ! is_array( $paths ) ) {
                $paths = array();
            }

            $normalized = array(
                self::TIER_FREE => array(),
                self::TIER_PRO  => array(),
            );

            foreach ( $normalized as $tier => $unused ) {
                if ( empty( $paths[ $tier ] ) || ! is_array( $paths[ $tier ] ) ) {
                    continue;
                }

                foreach ( $paths[ $tier ] as $path ) {
                    $path = trailingslashit( (string) $path );

                    if ( '' === $path || ! is_dir( $path ) ) {
                        continue;
                    }

                    if ( ! in_array( $path, $normalized[ $tier ], true ) ) {
                        $normalized[ $tier ][] = $path;
                    }
                }
            }

            if ( empty( $normalized[ self::TIER_PRO ] ) || ! $this->pro_available() ) {
                $normalized[ self::TIER_PRO ] = array();
            } elseif ( ! apply_filters( 'noesia_swa_pro_enabled', false, $this ) ) {
                $normalized[ self::TIER_PRO ] = array();
            }

            return $normalized;
        }

        /**
         * Discover and instantiate module files from one path.
         *
         * @param string $path Absolute directory path.
         *
         * @return void
         */
        private function discover_modules_in_path( $path ) {
            $path = trailingslashit( (string) $path );

            if ( ! is_dir( $path ) ) {
                return;
            }

            $files = $this->collect_module_files( $path );

            if ( empty( $files ) ) {
                return;
            }

            natcasesort( $files );

            foreach ( $files as $file ) {
                $this->load_module_file( $file );
            }
        }

        /**
         * Collect module entry files from a discovery path.
         *
         * Supports both legacy flat files and self-contained module folders.
         * Folder-based modules are authoritative when both structures exist.
         *
         * Examples:
         * - modules/free/class-noesia-swa-module-weather.php
         * - modules/free/weather/class-noesia-swa-module-weather.php
         *
         * @param string $path Absolute directory path.
         *
         * @return array<int, string>
         */
        private function collect_module_files( $path ) {
            $path        = trailingslashit( (string) $path );
            $module_map  = array();
            $subdirs     = glob( $path . '*', GLOB_ONLYDIR );
            $flat_files  = glob( $path . 'class-noesia-swa-module-*.php' );

            /*
             * Pass 1:
             * Folder-based modules win. This is the target architecture for
             * fully isolated, self-contained modules.
             */
            if ( is_array( $subdirs ) && ! empty( $subdirs ) ) {
                foreach ( $subdirs as $subdir ) {
                    if ( ! is_string( $subdir ) || ! is_dir( $subdir ) ) {
                        continue;
                    }

                    $subdir = trailingslashit( $subdir );
                    $slug   = sanitize_key( basename( untrailingslashit( $subdir ) ) );

                    if ( '' === $slug ) {
                        continue;
                    }

                    $entry_file = $subdir . 'class-noesia-swa-module-' . $slug . '.php';

                    if ( file_exists( $entry_file ) ) {
                        $module_map[ $slug ] = $entry_file;
                        continue;
                    }

                    $nested_files = glob( $subdir . 'class-noesia-swa-module-*.php' );

                    if ( ! is_array( $nested_files ) || empty( $nested_files ) ) {
                        continue;
                    }

                    natcasesort( $nested_files );

                    foreach ( $nested_files as $nested_file ) {
                        if ( ! is_string( $nested_file ) || ! file_exists( $nested_file ) ) {
                            continue;
                        }

                        $nested_slug = $this->extract_module_slug_from_file( $nested_file );

                        if ( '' === $nested_slug || isset( $module_map[ $nested_slug ] ) ) {
                            continue;
                        }

                        $module_map[ $nested_slug ] = $nested_file;
                    }
                }
            }

            /*
             * Pass 2:
             * Keep support for legacy flat files only when a folder-based
             * module with the same slug does not already exist.
             */
            if ( is_array( $flat_files ) && ! empty( $flat_files ) ) {
                foreach ( $flat_files as $flat_file ) {
                    if ( ! is_string( $flat_file ) || ! file_exists( $flat_file ) ) {
                        continue;
                    }

                    $flat_slug = $this->extract_module_slug_from_file( $flat_file );

                    if ( '' === $flat_slug || isset( $module_map[ $flat_slug ] ) ) {
                        continue;
                    }

                    $module_map[ $flat_slug ] = $flat_file;
                }
            }

            ksort( $module_map, SORT_NATURAL | SORT_FLAG_CASE );

            return array_values( $module_map );
        }

        /**
         * Extract a module slug from an entry file path.
         *
         * Example:
         * class-noesia-swa-module-screen-lock.php -> screen-lock
         *
         * @param string $file Absolute file path.
         *
         * @return string
         */
        private function extract_module_slug_from_file( $file ) {
            $file = basename( (string) $file );

            if ( ! preg_match( '/^class-noesia-swa-module-([a-z0-9\-]+)\.php$/', $file, $matches ) ) {
                return '';
            }

            if ( empty( $matches[1] ) ) {
                return '';
            }

            return sanitize_key( $matches[1] );
        }

        /**
         * Load a module file and register its module instance.
         *
         * @param string $file Absolute file path.
         *
         * @return void
         */
        private function load_module_file( $file ) {
            $file = (string) $file;

            if ( '' === $file || ! file_exists( $file ) ) {
                return;
            }

            try {
                require_once $file;

                $class_name = $this->resolve_module_class_name( $file );

                if ( '' === $class_name || ! class_exists( $class_name ) ) {
                    return;
                }

                if ( ! is_subclass_of( $class_name, 'Noesia_SWA_Module_Interface' ) ) {
                    return;
                }

                $module = new $class_name();

                if ( $module instanceof Noesia_SWA_Module_Interface ) {
                    $this->register_module( $module );
                }
            } catch ( Throwable $error ) {
                /**
                 * Fires when a module bootstrap throws during discovery.
                 *
                 * No core exception is rethrown here because one broken module
                 * should not take down the whole platform shell.
                 *
                 * @param Throwable          $error Runtime error/exception.
                 * @param string             $file  Module entry file.
                 * @param Noesia_SWA_Modules $this  Module manager instance.
                 */
                do_action( 'noesia_swa_module_boot_error', $error, $file, $this );
            }
        }

        /**
         * Resolve a module class name from a file name.
         *
         * Example:
         * class-noesia-swa-module-requests.php -> Noesia_SWA_Module_Requests
         *
         * @param string $file Absolute file path.
         *
         * @return string
         */
        private function resolve_module_class_name( $file ) {
            $base = basename( (string) $file, '.php' );
            $base = preg_replace( '/^class-/', '', $base );

            if ( empty( $base ) ) {
                return '';
            }

            $parts = explode( '-', $base );
            $parts = array_map( 'sanitize_key', $parts );
            $parts = array_filter( $parts );

            if ( empty( $parts ) ) {
                return '';
            }

            $class_parts = array();

            foreach ( $parts as $part ) {
                $class_parts[] = ucfirst( $part );
            }

            return implode( '_', $class_parts );
        }

        /**
         * Normalize a module tier.
         *
         * @param string $tier Raw tier value.
         *
         * @return string
         */
        private function normalize_tier( $tier ) {
            $tier = sanitize_key( (string) $tier );

            if ( self::TIER_FREE === $tier || self::TIER_PRO === $tier ) {
                return $tier;
            }

            return '';
        }
    }
}
