<?php
/**
 * @package WPFluent
 * @author  Sheikh Heera <heera.sheikh77@gmail.com> (https://heera.it)
 * @author  Sheikh Heera <mail@heera.it>
 * @author  Sheikh Heera <heera@authlab.io>
 * @link    https://github.com/wpfluent/framework2x
 * @license MIT https://opensource.org/licenses/MIT
 * @license GPL-2.0-or-later https://www.gnu.org/licenses/gpl-2.0.html
 */

namespace WPSocialReviews\Framework\Foundation;

use InvalidArgumentException;
use WPSocialReviews\Framework\Support\Arr;
use WPSocialReviews\Framework\Support\Env;
use WPSocialReviews\Framework\Http\Client;
use WPSocialReviews\Framework\Foundation\Config;
use WPSocialReviews\Framework\Container\Container;
use WPSocialReviews\Framework\Foundation\ComponentBinder;
use WPSocialReviews\Framework\Foundation\Concerns\FoundationTrait;

/**
 * Application — service container with magic property access via __get.
 *
 * Properties below are bound in {@see ComponentBinder::bindComponents()} and
 * {@see Application::init()}/{@see Application::setAppLevelNamespace()}. Each
 * `@property` declaration teaches PHPStan about a runtime container binding so
 * `$app->view->render(...)` and similar access can be type-checked.
 *
 * @property \WPSocialReviews\Framework\Foundation\Config        $config
 * @property \WPSocialReviews\Framework\View\View                $view
 * @property \WPSocialReviews\Framework\Cache\Cache              $cache
 * @property \WPSocialReviews\Framework\Http\Router\Router       $router
 * @property \WPSocialReviews\Framework\Http\Request\Request     $request
 * @property \WPSocialReviews\Framework\Http\Response\Response   $response
 * @property \WPSocialReviews\Framework\Validator\Validator      $validator
 * @property \WPSocialReviews\Framework\Events\Dispatcher        $events
 * @property \WPSocialReviews\Framework\Encryption\Encrypter     $encrypter
 * @property \WPSocialReviews\Framework\Encryption\Encrypter     $crypt
 * @property \WPSocialReviews\Framework\Database\DatabaseManager $db
 * @property \WPSocialReviews\Framework\Http\URL                 $url
 * @property \WPSocialReviews\Framework\Support\Mail             $mail
 * @property \WPSocialReviews\Framework\Support\Pipeline         $pipeline
 * @property string                             $__pluginfile__
 * @property string                             $__namespace__
 */
class Application extends Container
{
    use FoundationTrait;

    /**
     * Main plugin file's absolute path
     * 
     * @var string
     */
    protected $file = null;

    /**
     * Plugin's base url
     * 
     * @var string
     */
    protected $baseUrl = null;

    /**
     * Plugin's base path
     * 
     * @var string
     */
    protected $basePath = null;

    /**
     * Default namespace for hook's handlers
     * 
     * @var string
     */
    protected $handlerNamespace = null;

    /**
     * Default namespace for controllers
     * 
     * @var string
     */
    protected $controllerNamespace = null;

    /**
     * Default namespace for policy handlers
     * 
     * @var string
     */
    protected $policyNamespace = null;

    /**
     * Composer JSON
     * 
     * @var null|array
     */
    protected static $composer = null;

    /**
     * Ready event handlers
     * 
     * @var array
     */
    protected $onReady = [];

    /**
     * Flag to check if components are bound.
     * 
     * @var boolean
     */
    protected $componentsBound = false;

    /**
     * Construct the application instance
     * 
     * @param string $file The main plugin file's absolute path
     * @return null
     */
    public function __construct($file = null)
    {
        $this->init($file);
        $this->loadEnvironmentVars();
        $this->setAppLevelNamespace();
        $this->bootstrapApplication();
        $this->callPluginReadyCallbacks();
    }

    /**
     * Init the application instance
     * 
     * @param string $file The main plugin file's absolute path
     * 
     * @return null
     */
    protected function init($file)
    {
        $this['__pluginfile__'] = $this->file = $file;
        $this->basePath = plugin_dir_path($this->file);
        $this->baseUrl = plugin_dir_url($this->file);
    }

    /**
     * Load the environment bariables from .env file.
     * 
     * @param  string $path
     * @return void
     */
    protected function loadEnvironmentVars($path = null)
    {
        $path = $path ?: $this->basePath . '.env';

        is_readable($path) && Env::load($path);
    }

    /**
     * Set the default application level namespaces to resolve
     * the controllers, policies and various hook handlers.
     *
     * @return null
     */
    protected function setAppLevelNamespace()
    {
        $composer = $this->getComposer();

        $psr4 = array_flip($composer['autoload']['psr-4']);

        $this->policyNamespace = $psr4['app/'] . 'Http\Policies';

        $this->handlerNamespace = $psr4['app/'] . 'Hooks\Handlers';
        
        $this->controllerNamespace = $psr4['app/'] . 'Http\Controllers';

        $this['__namespace__'] = $composer['extra']['wpfluent']['namespace']['current'];
    }

    /**
     * Get the composer data as an array
     * 
     * @param  string $section Specific key
     * 
     * @return array partial or full composer data array
     */
    public function getComposer($section = null)
    {
        if (is_null(static::$composer)) {
            static::$composer = json_decode(
                file_get_contents($this->basePath . 'composer.json'), true
            );
        }

        return $section ? Arr::get(
            static::$composer, $section
        ) : static::$composer;
    }

    /**
     * Bootstrap the application.
     * 
     * @return null
     */
    protected function bootstrapApplication()
    {
        $this->bindAppInstance();
        $this->bindPathsAndUrls();
        $this->loadConfigIfExists();
        $this->registerMiddleware();
        $this->registerTextdomain();
        $this->bindCoreComponents();
        $this->requireCommonFiles($this);
        $this->addRestApiInitAction($this);
    }

    /**
     * Bind application instance in the container.
     * 
     * @return null
     */
    protected function bindAppInstance()
    {
        App::setInstance($this);
        $this->instance('app', $this);
        $this->instance(__CLASS__, $this);
        $this->instance('endpoints', []);
    }

    /**
     * Bind the paths and urls
     * 
     * @return null
     */
    protected function bindPathsAndUrls()
    {
        $this->bindUrls();
        $this->basePaths();
    }

    /**
     * Bind urls
     * 
     * @return null
     */
    protected function bindUrls()
    {
        $this['url.assets'] = $this->baseUrl . 'assets/';
    }

    /**
     * Bind paths
     * 
     * @return null
     */
    protected function basePaths()
    {
        $this['path'] = $this->basePath;
        $this['path.app'] = $this->basePath . 'app/';
        $this['path.hooks'] = $this['path.app'] . 'Hooks/';
        $this['path.http'] = $this['path.app'] . 'Http/';
        $this['path.controllers'] = $this['path.http'] . 'Controllers/';
        $this['path.config'] = $this->basePath . 'config/';
        $this['path.assets'] = $this->basePath . 'assets/';
        $this['path.resources'] = $this->basePath . 'resources/';
        $this['path.views'] = $this['path.app'] . 'Views/';
    }

    /**
     * Load application's config and set
     * the data in the Config instance.
     * 
     * @return null
     */
    protected function loadConfigIfExists()
    {
        $files = [];

        if (is_dir($this['path.config'])) {
            foreach (glob($this['path.config'] . '*.php') as $file) {
                // middleware.php lives under app/Http/ now; it ships closures
                // that don't belong in Config storage.
                if (basename($file) === 'middleware.php') {
                    continue;
                }
                $files[basename($file, '.php')] = $file;
            }
        }

        $this->instance('config', new Config([], $files));
    }

    /**
     * Register the HTTP middleware stack as a lazy container binding.
     *
     * Resolves from `app/Http/middleware.php` (canonical). Falls back to
     * `config/middleware.php` so un-migrated plugins keep working.
     *
     * @return void
     */
    protected function registerMiddleware()
    {
        $this->singleton('http.middleware', function ($app) {
            $new = $app['path.http'] . 'middleware.php';
            if (is_file($new)) {
                return require $new;
            }

            $legacy = $app['path.config'] . 'middleware.php';
            if (is_file($legacy)) {
                return require $legacy;
            }

            return [];
        });
    }

    /**
     * Resolve the given type from the container.
     * This method is required for unit testing.
     *
     * @param  string  $abstract
     * @param  array   $parameters
     * @return mixed
     */
    public function make($abstract, $parameters = [])
    {
        if (str_starts_with($abstract, '_NS')) {
            
            $namespace = $this->getComposer(
                'extra.wpfluent.namespace.current'
            );

            $abstract = str_replace('_NS', $namespace, $abstract);
        }
        
        return parent::make($abstract, $parameters);
    }

    /**
     * Register plugin's text domain
     * 
     * @return null
     */
    protected function registerTextdomain()
    {
        $this->addAction('init', function() {
            load_plugin_textdomain(
                $this->config->get(
                    'app.text_domain'
                ), false, $this->textDomainPath()
            );
        });
    }

    /**
     * Resolve the text domain path.
     * 
     * @return null
     */
    protected function textDomainPath()
    {
        return basename($this['path']) . $this->config->get('app.domain_path');
    }

    /**
     * Bind the components of the framework into the container so
     * they'll be available throughout the application life cycle.
     * 
     * @return null
     */
    protected function bindCoreComponents()
    {
        if (!$this->componentsBound) {
            $this->componentsBound = true;
            (new ComponentBinder($this))->bindComponents();
        }
    }

    /**
     * Load (include) the files where hooks are registered.
     * 
     * @param self $app
     * 
     * @return null
     */
    protected function requireCommonFiles($app)
    {
        $this->addFilter(
            'rest_pre_serve_request', [$this, 'preServeRequest'], 10, 4
        );

        require_once $this->basePath . 'app/Hooks/actions.php';
        require_once $this->basePath . 'app/Hooks/filters.php';
        
        if (file_exists($f = $this->basePath . 'app/Hooks/includes.php')) {
            require_once $f;
        }
    }

    /**
     * Handler for rest_pre_serve_request filter.
     * 
     * @param  bool $served  (default: false)
     * @param  \WP_REST_Response $result
     * @param  \WP_REST_Request  $request
     * @param  \WP_REST_Server   $server
     * @return bool (false to intercept, otherwise true)
     */
    public function preServeRequest($served, $result, $request, $server)
    {
        if ($result->get_status() === 404) {
            $route = $request->get_route();
            $slug = $this->config->get('app.slug');
            
            if ($this->isRequestOfPlugin($route, $slug)) {
                if ($this->isRequestForEndpoints($route)) {
                    status_header(200);
                    $result->set_status(200);
                    // @phpstan-ignore-next-line
                    $result->set_data($this->endpoints);
                } else {
                    $result->set_data(
                        $this->customizeNotFoundResponse(
                            $result, $request
                        )
                    );
                }
            }
        }

        return $served;
    }

    /**
     * Determines whether the request is madse by plugin.
     * 
     * @param  string  $route (Rest route|Full URL)
     * @param  string  $slug  (Plugin's slug)
     * @return bool
     */
    public function isRequestOfPlugin($route = '', $slug = '')
    {
        $slug = $slug ?: $this->config->get('app.slug');

        if (!$route) {
            if (get_option('permalink_structure')) {
                $route = $this->request->url();
            } else {
                $route = $this->request->query('rest_route');
            }
        }

        // For web routing (If web-routing is installed)
        if (!$route && !$this->request->isRest()) {
            $route = $this->request->url();
        }

        $parsedUrl = parse_url($route ?? '');
        
        $path = str_replace('/wp-json', '', $parsedUrl['path'] ?? '');

        if (is_admin()) {
            $page = $this->request->query('page');
            if ($slug === $page) {
                $path = $page;
            }
        } 

        return str_starts_with(ltrim($path, '/'), $slug);
    }

    /**
     * Determines if the request is made for endpoints.
     * 
     * @param  string  $route (Rest route|Full URL)

     * @return bool
     */
    protected function isRequestForEndpoints($route)
    {
        return str_ends_with($route, '__endpoints');
    }

    /**
     * Prepare a custom not found response.
     * 
     * @param  \WP_REST_Response $result
     * @param  \WP_REST_Request  $request
     * 
     * @return array
     */
    public function customizeNotFoundResponse($result, $request)
    {
        $response = $result->get_data();
        
        if ($this->env() === 'dev') {
            $response['data']['wpfluent'] = [
                'env' => $this->env(),
                'method' => $request->get_method(),
                'request_url' => $this->request->url(),
                'route_params' => $request->get_url_params(),
                'query_params' => $request->get_query_params(),
                'body_params' => $request->get_body_params(),
            ];
        } else {
            $response['data']['wpfluent'] = [
                'env' => $this->env()
            ];
        }

        return $response;
    }

    /**
     * Check if running unit test.
     * 
     * @return boolean
     */
    public function isUnitTesting()
    {
        return getenv('ENV') === 'testing';
    }

    /**
     * Register the rest api init actions and routes
     * 
     * @param self $app
     */
    protected function addRestApiInitAction($app)
    {
        $this->addAction('rest_api_init', function($wpRestServer) use ($app) {
            try {
                $this->registerRestRoutes($app->router);
            } catch (InvalidArgumentException $e) {
                return $app->response->json([
                    'message' => $e->getMessage()
                ], $e->getCode() ?: 500);
            }
        });
    }

    /**
     * Register rest routes.
     * 
     * @param \WPSocialReviews\Framework\Http\Router $router
     * 
     * @return void
     */
    protected function registerRestRoutes($router)
    {
        // @phpstan-ignore-next-line
        $router->registerRoutes(
            $this->requireRouteFile($router)
        );
    }

    /**
     * Load (include) routes
     * 
     * @param \WPSocialReviews\Framework\Http\Router $router
     * @return null
     */
    protected function requireRouteFile($router)
    {
        require_once $this['path.http'] . 'Routes/routes.php';
    }

    /**
     * Register plugin ready (booted) callbacks.
     * 
     * @param  callable $callback
     * @return void
     */
    public function ready(callable $callback)
    {
        $this->onReady[] = $callback;
    }

    /**
     * Execute plugin booted callbacks.
     * 
     * @return void
     */
    protected function callPluginReadyCallbacks()
    {
        $this->addAction('init', function() {
            while ($callback = array_pop($this->onReady)) {
                $callback($this);
            }
        });
    }
}
