<?php
/**
 * ModuleManager — Complete module lifecycle management with license awareness.
 *
 * Built-in module discovery:
 *   Every subdirectory of includes/modules/ that contains a module.json with
 *   at minimum `id` and `class` fields is treated as a built-in module.
 *   No PHP code change is needed when adding or removing a module — drop or
 *   delete its directory and the module.json inside it.
 *
 * Hook contract:
 *   ultraguard_pro/module_installed             ($module_id, $manifest)
 *   ultraguard_pro/module_uninstalled           ($module_id)
 *   ultraguard_pro/module_activated             ($module_id)
 *   ultraguard_pro/module_deactivated           ($module_id)
 *   ultraguard_pro/module_state_changed         ($module_id, $state)
 *   ultraguard_pro/module_definitions           (filter — array $definitions)
 *   ultraguard_pro/builtin_module_manifests     (filter — array $manifests)
 *
 * @package UltraguardPro\modules
 */

namespace UltraguardPro\modules;

use UltraguardPro\Plugin;
use UltraguardPro\support\LicenseInfo;

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

class ModuleManager {

    /** @var Plugin */
    private Plugin $plugin;

    /** @var string Absolute path to the third-party modules upload directory */
    private string $modules_dir;

    /** @var string URL to the third-party modules upload directory */
    private string $modules_url;

    private const INSTALLED_OPTION = 'ultraguard_installed_modules';
    private const ACTIVE_OPTION    = 'ultraguard_module_active_';
    private const DISABLED_OPTION  = 'ultraguard_module_disabled_';
    private const UPGRADE_URL      = 'https://ultraguard.net/pricing';

    public function __construct( Plugin $plugin ) {
        $this->plugin      = $plugin;
        $upload            = wp_upload_dir();
        $this->modules_dir = trailingslashit( $upload['basedir'] ) . 'ultraguard-modules/';
        $this->modules_url = trailingslashit( $upload['baseurl'] ) . 'ultraguard-modules/';
        wp_mkdir_p( $this->modules_dir );
    }

    // =========================================================================
    // PUBLIC API — Module Discovery & State
    // =========================================================================

    public function get_all_modules(): array {
        $built_in  = $this->get_builtin_manifests();
        $installed = $this->get_installed_manifests();
        $is_pro    = LicenseInfo::instance()->isPro();

        $all = array_merge( $built_in, $installed );

        foreach ( $all as $id => &$m ) {
            $license_required = $m['license_required'] ?? 'free';
            $license_ok       = ( $license_required === 'free' ) || $is_pro;

            $m['is_builtin']       = isset( $built_in[ $id ] );
            $m['is_active']        = $this->is_active( $id );
            $m['is_loaded']        = $this->plugin->modules->has( $id );
            $m['is_installed']     = true;
            $m['license_required'] = $license_required;
            $m['license_ok']       = $license_ok;
            $m['can_activate']     = $license_ok && ! $m['is_active'];
            $m['upgrade_url']      = ! $license_ok ? self::UPGRADE_URL : '';
        }
        unset( $m );

        return apply_filters( 'ultraguard_pro/module_manager/all_modules', $all );
    }

    public function activate( string $id ): array {
        if ( ! $this->module_exists( $id ) ) {
            /* translators: %s: string */
            return [ 'success' => false, 'message' => sprintf( __( 'Module "%s" not found.', 'ultraguard-security-suite' ), $id ) ];
        }

        $required = $this->get_license_required( $id );
        if ( $required === 'pro' && ! LicenseInfo::instance()->isPro() ) {
            return [
                'success'      => false,
                'message'      => __( 'This module requires UltraGuard Pro.', 'ultraguard-security-suite' ),
                'code'         => 'upgrade_required',
                'upgrade_url'  => self_admin_url( \UltraguardPro\support\UpgradeManager::UPGRADE_PAGE_URL_PATTERN ),
                'external_url' => \UltraguardPro\support\UpgradeManager::external_upgrade_url(),
            ];
        }

        $is_core = ( $this->get_builtin_manifests()[ $id ]['type'] ?? '' ) === 'core';
        if ( $is_core ) {
            return [ 'success' => false, 'message' => __( 'Core modules cannot be deactivated or re-activated.', 'ultraguard-security-suite' ) ];
        }

        delete_option( self::DISABLED_OPTION . $id );
        update_option( self::ACTIVE_OPTION   . $id, true );
        update_option( $id . '_enabled', true );
        delete_option( 'ultraguard_pro_db_version' );

        do_action( 'ultraguard_pro/module_activated',     $id );
        do_action( 'ultraguard_pro/module_state_changed', $id, 'activated' );

        return [
            'success'   => true,
            /* translators: %s: string */
            'message'   => sprintf( __( 'Module "%s" activated. The page will reload to apply changes.', 'ultraguard-security-suite' ), $this->get_module_name( $id ) ),
            'module_id' => $id,
            'reload'    => true,
        ];
    }

    public function deactivate( string $id ): array {
        if ( ! $this->module_exists( $id ) ) {
            /* translators: %s: string */
            return [ 'success' => false, 'message' => sprintf( __( 'Module "%s" not found.', 'ultraguard-security-suite' ), $id ) ];
        }

        // Guard core modules dynamically from the manifest — mirrors the is_core check
        // in activate(). Hardcoding IDs here would silently miss any new type:core modules.
        $is_core = ( $this->get_builtin_manifests()[ $id ]['type'] ?? '' ) === 'core';
        if ( $is_core ) {
            return [ 'success' => false, 'message' => __( 'Core modules cannot be deactivated.', 'ultraguard-security-suite' ) ];
        }

        update_option( self::DISABLED_OPTION . $id, true );
        update_option( self::ACTIVE_OPTION   . $id, false );
        update_option( $id . '_enabled', false );

        do_action( 'ultraguard_pro/module_deactivated',   $id );
        do_action( 'ultraguard_pro/module_state_changed', $id, 'deactivated' );

        return [
            'success'   => true,
            /* translators: %s: string */
            'message'   => sprintf( __( 'Module "%s" deactivated. The page will reload to apply changes.', 'ultraguard-security-suite' ), $this->get_module_name( $id ) ),
            'module_id' => $id,
            'reload'    => true,
        ];
    }

    public function install_from_zip( string $zip_path ): array {
        if ( ! file_exists( $zip_path ) ) {
            return [ 'success' => false, 'message' => __( 'ZIP file not found.', 'ultraguard-security-suite' ) ];
        }

        if ( ! LicenseInfo::instance()->isPro() ) {
            wp_delete_file( $zip_path );
            return [
                'success'      => false,
                'message'      => __( 'Uploading third-party modules requires UltraGuard Pro.', 'ultraguard-security-suite' ),
                'code'         => 'upgrade_required',
                'upgrade_url'  => self_admin_url( \UltraguardPro\support\UpgradeManager::UPGRADE_PAGE_URL_PATTERN ),
                'external_url' => \UltraguardPro\support\UpgradeManager::external_upgrade_url(),
            ];
        }

        $tmp = $this->modules_dir . 'tmp_' . time() . '/';
        wp_mkdir_p( $tmp );
        $unzipped = unzip_file( $zip_path, $tmp );
        wp_delete_file( $zip_path );

        if ( is_wp_error( $unzipped ) ) {
            $this->rmdir_recursive( $tmp );
            return [ 'success' => false, 'message' => $unzipped->get_error_message() ];
        }

        $dirs = glob( $tmp . '*', GLOB_ONLYDIR );
        if ( empty( $dirs ) ) {
            $this->rmdir_recursive( $tmp );
            return [ 'success' => false, 'message' => __( 'ZIP must contain exactly one module directory.', 'ultraguard-security-suite' ) ];
        }

        $module_tmp    = trailingslashit( $dirs[0] );
        $manifest_file = $module_tmp . 'module.json';

        if ( ! file_exists( $manifest_file ) ) {
            $this->rmdir_recursive( $tmp );
            return [ 'success' => false, 'message' => __( 'Missing module.json.', 'ultraguard-security-suite' ) ];
        }

        $manifest = json_decode( file_get_contents( $manifest_file ), true );
        if ( ! is_array( $manifest ) || empty( $manifest['id'] ) || empty( $manifest['class'] ) ) {
            $this->rmdir_recursive( $tmp );
            return [ 'success' => false, 'message' => __( 'Invalid module.json — must define at minimum: id, class.', 'ultraguard-security-suite' ) ];
        }

        $id = sanitize_key( $manifest['id'] );

        if ( $this->is_builtin( $id ) ) {
            $this->rmdir_recursive( $tmp );
            /* translators: %s: string */
            return [ 'success' => false, 'message' => sprintf( __( 'Cannot overwrite built-in module "%s".', 'ultraguard-security-suite' ), $id ) ];
        }

        if ( ! empty( $manifest['requires'] ) && version_compare( ULTRAGUARD_PRO_VERSION, $manifest['requires'], '<' ) ) {
            $this->rmdir_recursive( $tmp );
            /* translators: %s: string */
            return [ 'success' => false, 'message' => sprintf( __( 'Module requires UltraGuard Pro %s or newer.', 'ultraguard-security-suite' ), $manifest['requires'] ) ];
        }

        $dest = $this->modules_dir . $id . '/';
        if ( is_dir( $dest ) ) { $this->rmdir_recursive( $dest ); }

        // phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename -- moving validated extracted module directory into its final plugin-owned location.
        if ( ! rename( $module_tmp, $dest ) ) {
            $this->rmdir_recursive( $tmp );
            return [ 'success' => false, 'message' => __( 'Failed to move module files into place.', 'ultraguard-security-suite' ) ];
        }

        $this->rmdir_recursive( $tmp );

        $stored        = $this->get_installed_option();
        $stored[ $id ] = array_merge( $manifest, [ 'path' => $dest, 'installed_at' => current_time( 'mysql', true ) ] );
        update_option( self::INSTALLED_OPTION, $stored );

        $this->activate( $id );
        $this->maybe_install_module_schema( $id, $manifest, $dest );

        do_action( 'ultraguard_pro/module_installed', $id, $manifest );

        return [
            'success'   => true,
            /* translators: %s: string */
            'message'   => sprintf( __( 'Module "%s" installed and activated successfully.', 'ultraguard-security-suite' ), $manifest['name'] ?? $id ),
            'module_id' => $id,
            'reload'    => true,
        ];
    }

    public function uninstall( string $id ): array {
        if ( $this->is_builtin( $id ) ) {
            return [ 'success' => false, 'message' => __( 'Built-in modules cannot be uninstalled — use Deactivate instead.', 'ultraguard-security-suite' ) ];
        }

        $stored = $this->get_installed_option();
        if ( ! isset( $stored[ $id ] ) ) {
            return [ 'success' => false, 'message' => __( 'Module not found.', 'ultraguard-security-suite' ) ];
        }

        $manifest = $stored[ $id ];
        $name     = $manifest['name'] ?? $id;

        do_action( 'ultraguard_pro/module_before_uninstall', $id, $manifest );
        do_action( 'ultraguard_pro/module_cleanup_' . $id );

        $path = $this->modules_dir . $id . '/';
        if ( is_dir( $path ) ) { $this->rmdir_recursive( $path ); }

        unset( $stored[ $id ] );
        update_option( self::INSTALLED_OPTION, $stored );
        delete_option( self::ACTIVE_OPTION   . $id );
        delete_option( self::DISABLED_OPTION . $id );
        delete_option( $id . '_enabled' );

        do_action( 'ultraguard_pro/module_uninstalled',   $id );
        do_action( 'ultraguard_pro/module_state_changed', $id, 'uninstalled' );

        return [
            'success' => true,
            /* translators: %s: string */
            'message' => sprintf( __( 'Module "%s" uninstalled successfully.', 'ultraguard-security-suite' ), $name ),
            'reload'  => true,
        ];
    }

    // =========================================================================
    // PUBLIC HELPERS
    // =========================================================================

    public function get_modules_dir(): string { return $this->modules_dir; }
    public function get_modules_url(): string { return $this->modules_url; }
    public function get_upgrade_url(): string { return self::UPGRADE_URL; }

    public function is_builtin( string $id ): bool {
        return array_key_exists( $id, $this->get_builtin_manifests() );
    }

    public function is_active( string $id ): bool {
        if ( get_option( self::DISABLED_OPTION . $id ) ) { return false; }
        $active = get_option( self::ACTIVE_OPTION . $id );
        if ( $active !== false ) { return (bool) $active; }
        return $this->is_builtin( $id );
    }

    public function register_autoloaders(): void {
        foreach ( $this->get_installed_option() as $id => $manifest ) {
            $path = $this->modules_dir . $id . '/';
            if ( ! is_dir( $path ) ) { continue; }

            $fqcn      = $manifest['class'] ?? '';
            $parts     = explode( '\\', $fqcn );
            array_pop( $parts );
            $ns_prefix = implode( '\\', $parts );

            if ( $ns_prefix ) {
                spl_autoload_register( static function( string $class ) use ( $ns_prefix, $path ) {
                    if ( strpos( $class, $ns_prefix . '\\' ) !== 0 ) { return; }
                    $relative = substr( $class, strlen( $ns_prefix ) + 1 );
                    $parts    = explode( '\\', $relative );
                    $name     = array_pop( $parts );
                    $kebab    = strtolower( preg_replace( '/(?<!^)[A-Z]/', '-$0', $name ) );
                    $file     = $path . ( $parts ? strtolower( implode( '/', $parts ) ) . '/' : '' ) . 'class-' . $kebab . '.php';
                    if ( file_exists( $file ) ) { require_once $file; }
                } );
            } else {
                foreach ( glob( $path . '*.php' ) ?: [] as $f ) { require_once $f; }
            }
        }
    }

    public function get_extra_definitions(): array {
        $defs = [];
        foreach ( $this->get_installed_option() as $id => $manifest ) {
            if ( ! $this->is_active( $id ) )            { continue; }
            if ( empty( $manifest['class'] ) )           { continue; }
            if ( ! class_exists( $manifest['class'] ) ) { continue; }
            $defs[ $id ] = $manifest['class'];
        }
        return $defs;
    }

    /**
     * Return id => FQCN map for all built-in modules, sorted by priority.
     * Used by ModuleRegistry to build its boot list from module.json files.
     *
     * @return array<string, class-string>
     */
    public function get_builtin_class_definitions(): array {
        $manifests = $this->get_builtin_manifests();
        $defs      = [];
        foreach ( $manifests as $id => $manifest ) {
            $defs[ $id ] = $manifest['class'];
        }
        return $defs;
    }

    // =========================================================================
    // PRIVATE — Manifests & Internal Helpers
    // =========================================================================

    /**
     * Discover built-in module manifests by scanning includes/modules/{module}/module.json.
     *
     * Results are cached per-request in a static variable.
     * Strings are NOT wrapped in __() — fired before text domain is loaded.
     */
    private function get_builtin_manifests(): array {
        static $cache = null;
        if ( $cache !== null ) {
            return apply_filters( 'ultraguard_pro/builtin_module_manifests', $cache );
        }

        $manifests   = [];
        $modules_dir = __DIR__ . '/';

        foreach ( glob( $modules_dir . '*/module.json' ) ?: [] as $json_file ) {
            $raw = file_get_contents( $json_file );
            if ( ! $raw ) { continue; }

            $manifest = json_decode( $raw, true );
            if ( ! is_array( $manifest ) )    { continue; }
            if ( empty( $manifest['id'] ) )    { continue; }
            if ( empty( $manifest['class'] ) ) { continue; }

            $id = sanitize_key( $manifest['id'] );

            $manifests[ $id ] = [
                'id'               => $id,
                'name'             => $manifest['name']             ?? $id,
                'description'      => $manifest['description']      ?? '',
                'version'          => defined( 'ULTRAGUARD_PRO_VERSION' ) ? ULTRAGUARD_PRO_VERSION : '1.0.0',
                'class'            => $manifest['class'],
                'schema_class'     => $manifest['schema_class']     ?? '',
                'activation_dep'   => $manifest['activation_dep']   ?? '',
                'priority'         => (int) ( $manifest['priority'] ?? 50 ),
                'type'             => $manifest['type']             ?? 'feature',
                'icon'             => $manifest['icon']             ?? 'dashicons-admin-generic',
                'removable'        => (bool) ( $manifest['removable'] ?? false ),
                'license_required' => $manifest['license_required'] ?? 'free',
            ];
        }

        uasort( $manifests, static fn( $a, $b ) => $a['priority'] <=> $b['priority'] );

        $cache = $manifests;
        return apply_filters( 'ultraguard_pro/builtin_module_manifests', $cache );
    }

    private function get_installed_manifests(): array {
        $stored    = $this->get_installed_option();
        $manifests = [];

        foreach ( glob( $this->modules_dir . '*', GLOB_ONLYDIR ) ?: [] as $dir ) {
            $id   = basename( $dir );
            $file = trailingslashit( $dir ) . 'module.json';
            if ( ! file_exists( $file ) ) { continue; }

            $manifest = json_decode( file_get_contents( $file ), true );
            if ( ! is_array( $manifest ) || empty( $manifest['id'] ) ) { continue; }

            $manifests[ $id ] = array_merge(
                $manifest,
                $stored[ $id ] ?? [],
                [
                    'type'             => $manifest['type']             ?? 'feature',
                    'license_required' => $manifest['license_required'] ?? 'free',
                ]
            );
        }

        return $manifests;
    }

    private function get_installed_option(): array {
        return (array) get_option( self::INSTALLED_OPTION, [] );
    }

    private function module_exists( string $id ): bool {
        return $this->is_builtin( $id ) || isset( $this->get_installed_option()[ $id ] );
    }

    private function get_license_required( string $id ): string {
        $builtins = $this->get_builtin_manifests();
        if ( isset( $builtins[ $id ] ) ) { return $builtins[ $id ]['license_required'] ?? 'free'; }
        $installed = $this->get_installed_option();
        return $installed[ $id ]['license_required'] ?? 'free';
    }

    private function get_module_name( string $id ): string {
        $builtins = $this->get_builtin_manifests();
        if ( isset( $builtins[ $id ] ) ) { return $builtins[ $id ]['name'] ?? $id; }
        $installed = $this->get_installed_option();
        return $installed[ $id ]['name'] ?? $id;
    }

    private function rmdir_recursive( string $dir ): void {
        if ( ! is_dir( $dir ) ) { return; }
        foreach ( scandir( $dir ) ?: [] as $item ) {
            if ( $item === '.' || $item === '..' ) { continue; }
            $path = $dir . $item;
            is_dir( $path ) ? $this->rmdir_recursive( trailingslashit( $path ) ) : wp_delete_file( $path );
        }
        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir -- cleanup of empty temporary module directory.
        @rmdir( $dir );
    }

    private function maybe_install_module_schema( string $id, array $manifest, string $dest ): void {
        if ( ! function_exists( 'dbDelta' ) ) {
            require_once ABSPATH . 'wp-admin/includes/upgrade.php';
        }

        $module_class = $manifest['class'] ?? '';
        if ( $module_class && class_exists( $module_class ) && method_exists( $module_class, 'get_schema_class' ) ) {
            $schema_class = $module_class::get_schema_class();
            if ( $schema_class && class_exists( $schema_class ) ) {
                try {
                    $schema = new $schema_class();
                    if ( $schema instanceof \UltraguardPro\contracts\ModuleSchemaInterface ) {
                        $schema->install();
                        return;
                    }
                } catch ( \Throwable $e ) {
                    if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
                        \UltraguardPro\core\Logger::error( "Schema install failed for [{$id}]: " . $e->getMessage() );
                    }
                }
            }
        }

        $schema_dir = trailingslashit( $dest ) . 'schema/';
        if ( ! is_dir( $schema_dir ) ) { return; }

        foreach ( glob( $schema_dir . '*.php' ) ?: [] as $file ) { require_once $file; }

        foreach ( get_declared_classes() as $class ) {
            try {
                if ( ! ( new \ReflectionClass( $class ) )->implementsInterface( \UltraguardPro\contracts\ModuleSchemaInterface::class ) ) { continue; }
                $candidate = new $class();
                if ( $candidate->get_module_id() !== $id ) { continue; }
                $candidate->install();
                return;
            } catch ( \Throwable $e ) {
                if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
                    \UltraguardPro\core\Logger::error( "Schema auto-discover failed for [{$id}]: " . $e->getMessage() );
                }
                return;
            }
        }
    }
}
