<?php
/**
 * 'active' parameter in module.json is not taken in account.
 * Module 'active' flag is taken from DB.
 */

namespace App;

use App\Misc\WpApi;
use Illuminate\Database\Eloquent\Model;
use Symfony\Component\Console\Output\BufferedOutput;

class Module extends Model
{
    const IMG_DEFAULT = '/img/default-module.png';

    public $timestamps = false;

    /**
     * Modules list cached in memory.
     */
    public static $modules;

    public static function getCached()
    {
        if (!self::$modules) {
            // At this stage modules table may not exist
            try {
                self::$modules = self::all();
            } catch (\Exception $e) {
                // Do nothing
            }
        }

        return self::$modules;
    }

    public static function clearModulesCache()
    {
        self::$modules = null;
    }

    public static function isActive($alias)
    {
        $module = self::getByAlias($alias);
        if ($module) {
            return $module->active;
        } else {
            return false;
        }
    }

    public static function setActive($alias, $active, $save = true)
    {
        $module = self::getByAliasOrCreate($alias);
        $module->active = $active;
        if ($save) {
            try {
                $module->save();
            } catch (\Exception $e) {
                if (strstr($e->getMessage(), 'Integrity constraint violation')) {
                    // SQLSTATE[23000]: Integrity constraint violation.
                    self::clearModulesCache();
                    $module = self::getByAliasOrCreate($alias);
                    if ($module) {
                        $module->active = $active;
                        $module->save();
                    }
                } else {
                    throw $e;
                }
            }
        }

        return true;
    }

    /**
     * Is module license activated.
     */
    public static function isLicenseActivated($alias, $author_url)
    {
        // If module is from modules directory, license activation is required
        if ($author_url && self::isOfficial($author_url)) {
            $module = self::getByAlias($alias);
            if ($module) {
                return $module->activated;
            } else {
                return false;
            }
        } else {
            return true;
        }
    }

    public static function isOfficial($author_url)
    {
        return parse_url($author_url ?? '', PHP_URL_HOST) == parse_url(\Config::get('app.freescout_url') ?? '', PHP_URL_HOST);
    }

    /**
     * Activate module license.
     *
     * @param [type] $alias       [description]
     * @param [type] $details_url [description]
     *
     * @return bool [description]
     */
    public static function activateLicense($alias, $license)
    {
        $module = self::getByAliasOrCreate($alias);
        $module->license = $license;
        $module->activated = true;
        $module->save();
    }

    public static function deactivateLicense($alias, $license)
    {
        $module = self::getByAliasOrCreate($alias);
        $module->license = $license;
        $module->activated = false;
        $module->save();
    }

    public static function getByAliasOrCreate($alias)
    {
        $module = self::getByAlias($alias);
        if (!$module) {
            $module = new self();
            $module->alias = $alias;
        }

        return $module;
    }

    /**
     * Get module license.
     */
    public static function getLicense($alias)
    {
        $module = self::getByAlias($alias);
        if ($module) {
            return $module->license;
        } else {
            return '';
        }
    }

    public static function setLicense($alias, $license)
    {
        $module = self::getByAliasOrCreate($alias);
        $module->license = $license;
        $module->save();
    }

    /**
     * Automatically encrypt license key.
     */
    public function setLicenseAttribute($value)
    {
        if ($value != '') {
            $this->attributes['license'] = \Helper::encrypt($value);
        } else {
            $this->attributes['license'] = '';
        }
    }

    /**
     * Automatically decrypt license key.
     */
    public function getLicenseAttribute($value)
    {
        if (!$value) {
            return '';
        }

        return \Helper::decrypt($value) ?: $value;
    }

    public static function normalizeAlias($alias)
    {
        return trim(strtolower($alias));
    }

    public static function getByAlias($alias)
    {
        $modules = self::getCached();
        if ($modules) {
            return self::getCached()->where('alias', $alias)->first();
        } else {
            return;
        }
    }

    /**
     * Deactivate module and update modules cache.
     */
    public static function deactiveModule($alias, $clear_app_cache = true)
    {
        self::setActive($alias, false);
        // Update modules cache
        \Module::clearCache();
        if ($clear_app_cache) {
            \Artisan::call('freescout:clear-cache');
        }
    }

    /**
     * Get URL used to active and check license.
     *
     * @return [type] [description]
     */
    public static function getAppUrl()
    {
        return parse_url(\Config::get('app.url'), PHP_URL_HOST);
    }

    /**
     * Check missing extensions among required by module.
     *
     * @param [type] $required_extensions [description]
     *
     * @return [type] [description]
     */
    public static function getMissingExtensions($required_extensions)
    {
        $missing = [];

        $list = explode(',', $required_extensions ?? '');
        if (!is_array($list) || !count($list)) {
            return [];
        }
        foreach ($list as $ext) {
            $ext = trim($ext);
            if ($ext && !extension_loaded($ext)) {
                $missing[] = $ext;
            }
        }

        return $missing;
    }

    /**
     * Check missing modules required by the module.
     */
    public static function getMissingModules($required_modules, $modules = [])
    {
        $missing = [];

        if (!$modules) {
            $modules = \Module::all();
        }

        if (!is_array($required_modules) || !count($required_modules)) {
            return [];
        }
        foreach ($required_modules as $alias => $version) {
            $module = null;
            foreach ($modules as $module_item) {
                if ($module_item->alias == $alias) {
                    $module = $module_item;
                }
            }
            if (!$module) {
                $missing[$alias] = $version;
                continue;
            }

            if (!self::isActive($alias) || !version_compare($module->version, $version, '>=')) {
                $missing[$alias] = $version;
            }
        }

        return $missing;
    }

    public static function formatName($name)
    {
        return preg_replace("/ Module($|.*\[.*\]$)/", '$1', $name);
    }

    public static function formatModuleData($module_data)
    {
        // Add (Third-Party).
        if (self::isThirdParty($module_data) && mb_substr(trim($module_data['name']), -1) != ']') {
            $module_data['name'] = $module_data['name'].' ['.__('Third-Party').']';
        }
        return $module_data;
    }

    public static function isThirdParty($module_data)
    {
        if (\App\Module::isOfficial($module_data['detailsUrl']) 
            && $module_data['author'] != 'FreeScout'
        ) {
            return true;
        } else {
            return false;
        }
    }

    public static function getSymlinkPath($alias)
    {
        return public_path().\Module::getPublicPath($alias);
    }

    // Check and try to fix invalid or missing symlinks.
    public static function checkSymlinks($module_aliases = null)
    {
        $invalid_symlinks = [];

        if ($module_aliases === null) {
            // Get all active modules.
            foreach (\Module::all() as $module) {
                if ($module->active()) {
                    $module_aliases[] = $module->getAlias();
                }
            }
        }
        if ($module_aliases && count($module_aliases)) {
            foreach ($module_aliases as $module_alias) {
                $from = self::getSymlinkPath($module_alias);

                $create = false;

                // file_exists() also checks if symlink target exists.
                // file_exists() and is_dir() may throw "open_basedir restriction in effect".
                try {
                    if (!file_exists($from) || !is_link($from)) {
                        if (is_dir($from)) {
                            @rename($from, $from.'_'.date('YmdHis'));
                        } else {
                            @unlink($from);
                        }
                        $create = true;
                    } 
                } catch (\Exception $e) {
                    $create = true;
                }

                // Skip this check.
                // elseif (is_link($from) && readlink($symlink_path) != '') {
                //     // Symlink leads to the wrong place.
                //     $create = true;
                // }

                // Try to create the symlink.
                if ($create) {
                    $to = self::createModuleSymlink($module_alias);

                    if ($to && (!is_link($from) || is_link($to) || !file_exists($from))) {
                        $invalid_symlinks[$from] = $to;
                    }
                }
            }
        }

        return $invalid_symlinks;
    }

    // There is similar function in ModuleInstall.php
    public static function createModuleSymlink($alias)
    {
        $from = self::getSymlinkPath($alias);

        $module = \Module::findByAlias($alias);
        if (!$module) {
            return false;
        }

        $to = $module->getExtraPath('Public');

        // file_exists() may throw "open_basedir restriction in effect".
        try {
            // If module's Public is symlink.
            if (is_link($to)) {
                @unlink($to);
            }

            // Symlimk may exist but lead to the module folder in a wrong case.
            // So we need first try to remove it.
            if (!file_exists($from)) {
                @unlink($from);
            }

            if (file_exists($from)) {
                return $to;
            }

            if (!file_exists($to)) {
                // Try to create Public folder.
                try {
                    \File::makeDirectory($to, \Helper::DIR_PERMISSIONS);
                } catch (\Exception $e) {
                    // If it's a broken symlink.
                    if (is_link($to)) {
                        @unlink($to);
                    }
                }
            }

            try {
                symlink($to, $from);
            } catch (\Exception $e) {
                \Log::error('Error occurred creating ['.$from.' » '.$to.'] symlink: '.$e->getMessage());
                //return false;
            }
        } catch (\Exception $e) {
            return false;
        }

        return $to;
    }

    public static function updateModule($alias)
    {
        $result = [
            'status' => 'error',
            // Error message.
            'msg' => '',
            'msg_success' => '',
            'download_error' => false,
            // Error message with the link for downloading the module.
            'download_msg' => '',
            'output' => '',
            'module_name' => '',
        ];

        $module = \Module::findByAlias($alias);

        if (!$module) {
            $result['msg'] = __('Module not found').': '.$alias;
        }

        // Get module name.
        $name = '?';
        if ($module) {
            $name = $module->getName();
            $result['module_name'] = $name;
        }

        // Download new version.
        if (!$result['msg']) {
            // Check if the module's author is officially recognized
            if (self::isOfficial($module->authorUrl)) {
                $params = [
                    'license'      => self::getLicense($alias),
                    'module_alias' => $alias,
                    'url'          => self::getAppUrl(),
                ];
                $license_details = WpApi::getVersion($params);

                if (WpApi::$lastError) {
                    $result['msg'] = WpApi::$lastError['message'];
                } elseif (!empty($license_details['code']) && !empty($license_details['message'])) {
                    $result['msg'] = $license_details['message'];
                } elseif (!empty($license_details['required_app_version']) && !\Helper::checkAppVersion($license_details['required_app_version'])) {
                    $result['msg'] = 'Module requires app version:'.' '.$license_details['required_app_version'];
                } elseif (!empty($license_details['download_link'])) {
                    // If a download link is available, proceed to update the module from the URL
                    $result = self::updateFromUrl($module, $license_details['download_link'], $result);
                } elseif ($license_details['status'] && $result['msg'] = self::getErrorMessage($license_details['status'])) {
                    //$result['msg'] = ;
                } else {
                    $result['msg'] = __('Error occurred').': '.json_encode($license_details);
                }
            } else {
                // If the module's author is not officially recognized, check for a direct download link
                $latest_version_zip_url = $module->latestVersionZipUrl ?? null;

                if (!empty($latest_version_zip_url)) {
                    // Update the module from the provided ZIP URL
                    $result = self::updateFromUrl($module, $module->latestVersionZipUrl, $result);
                } else {
                    // If no download link is available, set an error message indicating the module cannot be downloaded
                    $result['msg'] = __('Error occurred') . ': module not available for download';
                }
            }
        }

        // Run post-update instructions.
        if (!$result['msg'] && !$result['download_error']) {
            $output_log = new BufferedOutput();
            \Artisan::call('freescout:module-install', ['module_alias' => $alias], $output_log);
            $result['output'] = $output_log->fetch() ?: ' ';

            $result['msg'] = __('Error occurred activating ":name" module', ['name' => $name]);

            if (session('flashes_floating') && is_array(session('flashes_floating'))) {
                // Error.
                // If there was any error, module has been deactivated via modules.register_error filter
                $result['msg'] = '';
                foreach (session('flashes_floating') as $flash) {
                    $result['msg'] .= $flash['text'].' ';
                }
            } elseif (strstr($result['output'], 'Configuration cached successfully')) {
                // Success.
                $result['status'] = 'success';
                $result['msg'] = '';
                $result['msg_success'] = __('":name" module successfully updated!', ['name' => $name]);
            } else {
                // Error.
                // Deactivate module.
                \App\Module::setActive($alias, false);
                \Artisan::call('freescout:clear-cache');
            }
        }

        return $result;
    }

    /**
     * Updates a module from a given URL.
     *
     * @param Module $module The module to be updated.
     * @param string $url The URL where the new version of the module can be downloaded.
     * @param array $result An associative array to store the result of the update operation.
     *
     * @return void
     */
    private static function updateFromUrl($module, $url, $result)
    {
        $alias = $module->alias;
        // Download module.
        $module_archive = \Module::getPath() . DIRECTORY_SEPARATOR . $alias . '.zip';

        try {
            \Helper::downloadRemoteFile($url, $module_archive);
        } catch (\Exception $e) {
            $result['msg'] = $e->getMessage();
        }

        if (!file_exists($module_archive)) {
            $result['download_error'] = true;
        } else {
            // Extract.
            try {
                $module_path = $module->getPath();

                // Sometimes by some reason Public folder becomes a symlink leading to itself.
                // It causes an error during updating process.
                // https://github.com/freescout-helpdesk/freescout/issues/2709
                $public_folder = $module_path.DIRECTORY_SEPARATOR.'Public';
                try {
                    if (is_link($public_folder)) {
                        unlink($public_folder);
                    }
                } catch (\Exception $e) {
                    // Do nothing.
                }

                \Helper::unzip($module_archive, \Module::getPath());

                // Rename the folder if the archive has been downloaded from GitHub:
                // SomeModule-master >> SomeModule.
                // https://github.com/freescout-help-desk/freescout/issues/4611
                $basename = basename($url, '.zip');

                if (file_exists($module_path.'-'.$basename)) {
                    \File::moveDirectory($module_path.'-'.$basename, $module_path, true);
                }
            } catch (\Exception $e) {
                $result['msg'] = $e->getMessage();
            }
            // Check if extracted module exists.
            \Module::clearCache();
            $module = \Module::findByAlias($alias);
            if (!$module) {
                $result['download_error'] = true;
            }
        }

        // Remove archive.
        if (file_exists($module_archive)) {
            \File::delete($module_archive);
        }

        if ($result['download_error']) {
            $result['download_msg'] = __('Error occurred downloading the module. Please :%a_being%download:%a_end% module manually and extract into :folder', [
                '%a_being%' => '<a href="' . $url . '" target="_blank">',
                '%a_end%'   => '</a>',
                'folder'    => '<strong>' . \Module::getPath() . '</strong>',
            ]);
        }

        return $result;
    }

    public static function getErrorMessage($code, $result = null)
    {
        $msg = '';

        switch ($code) {
            case 'missing':
                $msg = __('License key does not exist');
                break;
            case 'license_not_activable':
                $msg = __("You have to activate each bundle's module separately");
                break;
            case 'disabled':
                $msg = __('License key has been revoked');
                break;
            case 'no_activations_left':
                $msg = __('No activations left for this license key').' ('.__("Use 'Deactivate License' link above to transfer license key from another domain").')';
                break;
            case 'expired':
                $msg = __('License key has expired');
                break;
            case 'key_mismatch':
                $msg = __('License key belongs to another module');
                break;
            // This also happens when entering a valid license key for wrong module.
            case 'invalid_item_id':
                $msg = __('Invalid license key');
                //$msg = __('Module not found in the modules directory');
                break;
            case 'site_inactive':
                $msg = __('License key is activated on another domain.').' '.__("Use 'Deactivate License' link above to transfer license key from another domain");
                //$msg = __('Module not found in the modules directory');
                break;
            default:
                if ($result && !empty($result['error'])) {
                    $msg = __('Error code:'.' '.$result['error']);
                }
                break;
        }

        return $msg;
    }
}