<?php
/**
 * Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
 *
 * This file is part of a Moko Consulting project.
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 */

/**
 * MokoCassiopeia install/update/uninstall script.
 *
 * On update: copies the template as MokoOnyx (new directory), updates the
 * database to register MokoOnyx, migrates styles + params, and sets it as
 * the default site template. The old MokoCassiopeia directory stays intact
 * (Joomla's installer still needs it) — the user can uninstall it later.
 */

defined('_JEXEC') or die;

use Joomla\CMS\Factory;
use Joomla\CMS\Installer\InstallerAdapter;
use Joomla\CMS\Installer\InstallerScriptInterface;
use Joomla\CMS\Log\Log;

class Tpl_MokocassiopeiaInstallerScript implements InstallerScriptInterface
{
    private const MIN_PHP    = '8.1.0';
    private const MIN_JOOMLA = '4.4.0';

    private const OLD_NAME    = 'mokocassiopeia';
    private const NEW_NAME    = 'mokoonyx';
    private const OLD_DISPLAY = 'MokoCassiopeia';
    private const NEW_DISPLAY = 'MokoOnyx';

    private const ONYX_UPDATES_URL = 'https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/main/updates.xml';

    // ── Joomla lifecycle ───────────────────────────────────────────────

    public function preflight(string $type, InstallerAdapter $parent): bool
    {
        if (version_compare(PHP_VERSION, self::MIN_PHP, '<')) {
            Factory::getApplication()->enqueueMessage(
                sprintf('MokoCassiopeia requires PHP %s+. Running %s.', self::MIN_PHP, PHP_VERSION),
                'error'
            );
            return false;
        }

        if (version_compare(JVERSION, self::MIN_JOOMLA, '<')) {
            Factory::getApplication()->enqueueMessage(
                sprintf('MokoCassiopeia requires Joomla %s+. Running %s.', self::MIN_JOOMLA, JVERSION),
                'error'
            );
            return false;
        }

        return true;
    }

    public function install(InstallerAdapter $parent): bool
    {
        $this->log('MokoCassiopeia installed.');
        return true;
    }

    public function update(InstallerAdapter $parent): bool
    {
        $this->log('MokoCassiopeia update() — version ' . ($parent->getManifest()->version ?? '?'));
        return true;
    }

    public function uninstall(InstallerAdapter $parent): bool
    {
        $this->log('MokoCassiopeia uninstalled.');
        return true;
    }

    public function postflight(string $type, InstallerAdapter $parent): bool
    {
        // Debug: write directly to confirm script runs
        @file_put_contents(
            JPATH_ROOT . '/administrator/logs/bridge_debug.txt',
            date('Y-m-d H:i:s') . " postflight called, type={$type}\n",
            FILE_APPEND
        );

        if ($type === 'update') {
            $this->log('=== MokoCassiopeia → MokoOnyx bridge ===');
            $this->bridge();
        }

        return true;
    }

    // ── Bridge ─────────────────────────────────────────────────────────

    private function bridge(): void
    {
        $app = Factory::getApplication();

        // 1. Copy template directory (don't rename — Joomla still needs the old one)
        $copied = $this->copyTemplateDir();
        if (!$copied && !is_dir(JPATH_ROOT . '/templates/' . self::NEW_NAME)) {
            $app->enqueueMessage(
                'MokoOnyx bridge: could not create template directory. '
                . 'Please copy <code>templates/mokocassiopeia</code> to <code>templates/mokoonyx</code> manually.',
                'warning'
            );
            return;
        }

        // 2. Copy media directory
        $this->copyMediaDir();

        // 3. Register MokoOnyx in #__extensions (if not already there)
        $this->registerExtension();

        // 4. Migrate template styles (create MokoOnyx styles with same params)
        $this->migrateStyles();

        // 5. Redirect update server to MokoOnyx
        $this->updateUpdateServer();

        // 6. Unlock MokoCassiopeia (allow uninstall) + lock MokoOnyx (prevent accidental uninstall)
        $this->updateExtensionLocks();

        // 7. Notify
        $app->enqueueMessage(
            '<strong>MokoOnyx has been installed as a replacement for MokoCassiopeia.</strong><br>'
            . 'Your template settings have been migrated. MokoOnyx is now your active site template.<br>'
            . 'MokoCassiopeia has been unlocked — you can uninstall it from Extensions &rarr; Manage.',
            'success'
        );

        $this->log('=== Bridge completed ===');
    }

    // ── Copy directories ───────────────────────────────────────────────

    private function copyTemplateDir(): bool
    {
        $src = JPATH_ROOT . '/templates/' . self::OLD_NAME;
        $dst = JPATH_ROOT . '/templates/' . self::NEW_NAME;

        if (is_dir($dst)) {
            $this->log('Bridge: templates/' . self::NEW_NAME . ' already exists — skipping copy.');
            return true;
        }

        if (!is_dir($src)) {
            $this->log('Bridge: source template dir not found.', 'error');
            return false;
        }

        $result = $this->recursiveCopy($src, $dst);
        $this->log('Bridge: ' . ($result ? 'copied' : 'FAILED to copy') . ' template dir → ' . self::NEW_NAME);
        return $result;
    }

    private function copyMediaDir(): void
    {
        $src = JPATH_ROOT . '/media/templates/site/' . self::OLD_NAME;
        $dst = JPATH_ROOT . '/media/templates/site/' . self::NEW_NAME;

        if (is_dir($dst)) {
            $this->log('Bridge: media dir already exists — skipping.');
            return;
        }

        if (!is_dir($src)) {
            $this->log('Bridge: source media dir not found — skipping.');
            return;
        }

        $result = $this->recursiveCopy($src, $dst);
        $this->log('Bridge: ' . ($result ? 'copied' : 'FAILED to copy') . ' media dir → ' . self::NEW_NAME);
    }

    private function recursiveCopy(string $src, string $dst): bool
    {
        if (!mkdir($dst, 0755, true) && !is_dir($dst)) {
            return false;
        }

        $dir = opendir($src);
        if ($dir === false) {
            return false;
        }

        while (($file = readdir($dir)) !== false) {
            if ($file === '.' || $file === '..') {
                continue;
            }

            $srcPath = $src . '/' . $file;
            $dstPath = $dst . '/' . $file;

            if (is_dir($srcPath)) {
                $this->recursiveCopy($srcPath, $dstPath);
            } else {
                copy($srcPath, $dstPath);
            }
        }

        closedir($dir);
        return true;
    }

    // ── Database updates ───────────────────────────────────────────────

    private function registerExtension(): void
    {
        $db = Factory::getDbo();

        // Check if MokoOnyx is already registered
        $query = $db->getQuery(true)
            ->select('extension_id')
            ->from('#__extensions')
            ->where($db->quoteName('element') . ' = ' . $db->quote(self::NEW_NAME))
            ->where($db->quoteName('type') . ' = ' . $db->quote('template'));
        $exists = (int) $db->setQuery($query)->loadResult();

        if ($exists) {
            $this->log('Bridge: MokoOnyx already registered in #__extensions (id=' . $exists . ').');
            return;
        }

        // Copy the MokoCassiopeia extension row and change element/name
        $query = $db->getQuery(true)
            ->select('*')
            ->from('#__extensions')
            ->where($db->quoteName('element') . ' = ' . $db->quote(self::OLD_NAME))
            ->where($db->quoteName('type') . ' = ' . $db->quote('template'));
        $oldExt = $db->setQuery($query)->loadObject();

        if (!$oldExt) {
            $this->log('Bridge: MokoCassiopeia not found in #__extensions.', 'warning');
            return;
        }

        $newExt = clone $oldExt;
        unset($newExt->extension_id);
        $newExt->element = self::NEW_NAME;
        $newExt->name    = self::NEW_NAME;

        // Update manifest_cache to reflect new name
        if (is_string($newExt->manifest_cache)) {
            $newExt->manifest_cache = str_replace(self::OLD_NAME, self::NEW_NAME, $newExt->manifest_cache);
            $newExt->manifest_cache = str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $newExt->manifest_cache);
        }

        try {
            $db->insertObject('#__extensions', $newExt, 'extension_id');
            $this->log('Bridge: registered MokoOnyx in #__extensions (id=' . $newExt->extension_id . ').');
        } catch (\Throwable $e) {
            $this->log('Bridge: failed to register extension: ' . $e->getMessage(), 'error');
        }
    }

    private function migrateStyles(): void
    {
        $db = Factory::getDbo();

        // Get all MokoCassiopeia styles
        $query = $db->getQuery(true)
            ->select('*')
            ->from('#__template_styles')
            ->where($db->quoteName('template') . ' = ' . $db->quote(self::OLD_NAME))
            ->where($db->quoteName('client_id') . ' = 0');
        $oldStyles = $db->setQuery($query)->loadObjectList();

        if (empty($oldStyles)) {
            $this->log('Bridge: no MokoCassiopeia styles found.');
            return;
        }

        $this->log('Bridge: migrating ' . count($oldStyles) . ' style(s).');

        foreach ($oldStyles as $old) {
            $newTitle = str_replace(self::OLD_DISPLAY, self::NEW_DISPLAY, $old->title);
            $newTitle = str_replace(self::OLD_NAME, self::NEW_NAME, $newTitle);

            // Skip if MokoOnyx already has this style
            $check = $db->getQuery(true)
                ->select('COUNT(*)')
                ->from('#__template_styles')
                ->where($db->quoteName('template') . ' = ' . $db->quote(self::NEW_NAME))
                ->where($db->quoteName('title') . ' = ' . $db->quote($newTitle));
            if ((int) $db->setQuery($check)->loadResult() > 0) {
                $this->log("Bridge: style '{$newTitle}' already exists — skipping.");
                continue;
            }

            $newParams = is_string($old->params)
                ? str_replace(self::OLD_NAME, self::NEW_NAME, $old->params)
                : $old->params;

            $new = clone $old;
            unset($new->id);
            $new->template = self::NEW_NAME;
            $new->title    = $newTitle;
            $new->params   = $newParams;
            $new->home     = 0;

            try {
                $db->insertObject('#__template_styles', $new, 'id');
                $newId = $new->id;

                // If old was default, make new default
                if ($old->home == 1) {
                    $db->setQuery(
                        $db->getQuery(true)
                            ->update('#__template_styles')
                            ->set($db->quoteName('home') . ' = 1')
                            ->where('id = ' . (int) $newId)
                    )->execute();

                    $db->setQuery(
                        $db->getQuery(true)
                            ->update('#__template_styles')
                            ->set($db->quoteName('home') . ' = 0')
                            ->where('id = ' . (int) $old->id)
                    )->execute();

                    $this->log('Bridge: set MokoOnyx as default site template.');
                }

                $this->log("Bridge: created style '{$newTitle}'.");
            } catch (\Throwable $e) {
                $this->log("Bridge: failed to create style '{$newTitle}': " . $e->getMessage(), 'warning');
            }
        }
    }

    private function updateUpdateServer(): void
    {
        $db = Factory::getDbo();

        try {
            $query = $db->getQuery(true)
                ->update('#__update_sites')
                ->set($db->quoteName('location') . ' = ' . $db->quote(self::ONYX_UPDATES_URL))
                ->set($db->quoteName('name') . ' = ' . $db->quote(self::NEW_DISPLAY))
                ->where($db->quoteName('location') . ' LIKE ' . $db->quote('%MokoCassiopeia%'));
            $db->setQuery($query)->execute();

            $n = $db->getAffectedRows();
            if ($n > 0) {
                $this->log("Bridge: redirected {$n} update site(s) to MokoOnyx.");
            }
        } catch (\Throwable $e) {
            $this->log('Bridge: update server redirect failed: ' . $e->getMessage(), 'warning');
        }

        // Clear cached updates
        try {
            $db->setQuery(
                $db->getQuery(true)
                    ->delete('#__updates')
                    ->where($db->quoteName('element') . ' = ' . $db->quote(self::OLD_NAME))
            )->execute();
        } catch (\Throwable $e) {
            // Not critical
        }
    }

    private function updateExtensionLocks(): void
    {
        $db = Factory::getDbo();

        // Unlock MokoCassiopeia — allow uninstall
        try {
            $query = $db->getQuery(true)
                ->update('#__extensions')
                ->set($db->quoteName('locked') . ' = 0')
                ->set($db->quoteName('protected') . ' = 0')
                ->where($db->quoteName('element') . ' = ' . $db->quote(self::OLD_NAME))
                ->where($db->quoteName('type') . ' = ' . $db->quote('template'));
            $db->setQuery($query)->execute();
            if ($db->getAffectedRows() > 0) {
                $this->log('Bridge: unlocked MokoCassiopeia (can be uninstalled).');
            }
        } catch (\Throwable $e) {
            $this->log('Bridge: failed to unlock MokoCassiopeia: ' . $e->getMessage(), 'warning');
        }

        // Lock MokoOnyx — prevent accidental uninstall
        try {
            $query = $db->getQuery(true)
                ->update('#__extensions')
                ->set($db->quoteName('locked') . ' = 1')
                ->where($db->quoteName('element') . ' = ' . $db->quote(self::NEW_NAME))
                ->where($db->quoteName('type') . ' = ' . $db->quote('template'));
            $db->setQuery($query)->execute();
            if ($db->getAffectedRows() > 0) {
                $this->log('Bridge: locked MokoOnyx (protected from uninstall).');
            }
        } catch (\Throwable $e) {
            $this->log('Bridge: failed to lock MokoOnyx: ' . $e->getMessage(), 'warning');
        }
    }

    // ── Logging ────────────────────────────────────────────────────────

    private function log(string $message, string $priority = 'info'): void
    {
        static $init = false;
        if (!$init) {
            Log::addLogger(
                ['text_file' => 'mokocassiopeia_bridge.log.php'],
                Log::ALL,
                ['mokocassiopeia_bridge']
            );
            $init = true;
        }

        $levels = ['info' => Log::INFO, 'warning' => Log::WARNING, 'error' => Log::ERROR];
        Log::add($message, $levels[$priority] ?? Log::INFO, 'mokocassiopeia_bridge');
    }
}
