<?php

declare(strict_types=1);

namespace Plugin\tnt_mcp\adminmenu\Controller;

use JTL\DB\DbInterface;
use JTL\Plugin\PluginInterface;
use JTL\Shop;
use JTL\Smarty\JTLSmarty;
use Plugin\tnt_mcp\src\Mcp\Admin\AdminLanguage;
use Plugin\tnt_mcp\src\Mcp\Admin\TabUrl;
use Throwable;

/**
 * Shared plumbing for the plugin's admin-menu tab controllers. Each concrete controller reads a
 * (read-only) view model from src/ and renders a Smarty template, echoing the HTML — JTL captures
 * it via ob_start in PluginController::renderMenu. Rendering is gated on a logged-in backend admin
 * as belt-and-suspenders; the backend already requires login to reach here.
 */
abstract class BaseAdminController
{
    /** Relative to the admin working dir, matching the sibling-plugin convention. */
    protected const SMARTY_PATH = '../plugins/tnt_mcp/adminmenu/templates/';

    protected JTLSmarty $smarty;

    private ?string $language = null;

    /**
     * @param string $tabName the Customlink name JTL dispatched, e.g. 'Änderungen'. Needed by
     *        {@see selfUrl()} and empty only when a caller (a test) does not care about links.
     */
    public function __construct(protected PluginInterface $plugin, protected string $tabName = '')
    {
        $this->smarty = Shop::Smarty();
    }

    /** Render the tab, echoing its HTML. */
    abstract public function render(): void;

    /**
     * A link back to THIS tab of THIS plugin page — {@see TabUrl} explains why the tab has to be
     * named in the link at all.
     *
     * @param array<string, string> $overrides added or replaced; an empty-string value REMOVES the
     *        parameter, which is how a detail link goes back to being a list link
     */
    protected function selfUrl(array $overrides = []): string
    {
        return TabUrl::build($_GET, $overrides, $this->tabName);
    }

    protected function isAdmin(): bool
    {
        try {
            return Shop::Container()->getAdminAccount()->logged();
        } catch (Throwable) {
            return false;
        }
    }

    protected function db(): DbInterface
    {
        return Shop::Container()->getDB();
    }

    protected function pluginVersion(): string
    {
        try {
            // getVersion() is int|string in the shop's plugin meta (a numeric-only version is
            // returned as an int) — the templates always want the string form.
            return (string)$this->plugin->getMeta()->getVersion();
        } catch (Throwable) {
            return '';
        }
    }

    /** @return non-empty-string the shop URL, or a localhost fallback — never empty */
    protected function baseUrl(): string
    {
        $url = Shop::getURL();

        return $url !== '' ? $url : 'https://localhost';
    }

    /**
     * The admin-UI language for this request — see {@see AdminLanguage}. Resolved once per render so
     * every string on a tab comes from the same language, even if the session changed mid-request.
     */
    protected function language(): string
    {
        return $this->language ??= AdminLanguage::current();
    }

    /**
     * Shorthand for {@see AdminLanguage::pick()} bound to {@see self::language()}.
     *
     * @param array{de: string, en: string} $texts
     */
    protected function text(array $texts): string
    {
        return AdminLanguage::pick($texts, $this->language());
    }

    /**
     * Assigns the template vars plus `lang`, which every tab template branches on — a template must
     * never have to call into PHP to find out which language it is rendering — `pluginURL` for
     * asset links, and `ui` ({@see Ui}) so no tab invents its own class vocabulary. The result is wrapped by {@see shell()}, so a template never carries the
     * stylesheet link or the Tailwind scope itself.
     *
     * @param array<string,mixed> $vars
     */
    protected function fetch(string $template, array $vars): string
    {
        $vars['lang']      = $vars['lang'] ?? $this->language();
        $vars['pluginURL'] = $this->adminUrl();
        $vars['ui']        = Ui::vars();
        foreach ($vars as $key => $value) {
            $this->smarty->assign($key, $value);
        }

        try {
            return $this->shell($this->smarty->fetch(self::SMARTY_PATH . $template));
        } catch (Throwable $e) {
            return '<div class="alert alert-danger">tnt_mcp: '
                . htmlspecialchars($e->getMessage(), \ENT_QUOTES) . '</div>';
        }
    }

    /**
     * One template rendered WITHOUT the stylesheet/scope wrapper, for a tab that composes another
     * tab's view (see {@see Activity}).
     *
     * Smarty resolves `{include file=…}` against its own template dirs, which are the shop admin's
     * — not this plugin's — so a sibling template cannot include a sibling template by name and the
     * relative path this class fetches with does not resolve there either. Composing in PHP is the
     * way that works, and it keeps the shell exactly once per request.
     *
     * @param array<string,mixed> $vars
     */
    protected function partial(string $template, array $vars): string
    {
        $vars['lang']      = $vars['lang'] ?? $this->language();
        $vars['pluginURL'] = $this->adminUrl();
        $vars['ui']        = Ui::vars();
        foreach ($vars as $key => $value) {
            $this->smarty->assign($key, $value);
        }

        try {
            return $this->smarty->fetch(self::SMARTY_PATH . $template);
        } catch (Throwable $e) {
            return '<div class="alert alert-danger">tnt_mcp: '
                . htmlspecialchars($e->getMessage(), \ENT_QUOTES) . '</div>';
        }
    }

    /**
     * The plugin's compiled Tailwind wrapped around a tab's HTML.
     *
     * Every utility this plugin ships is scoped to `.tailwind` (`important` in
     * adminmenu/tailwind.config.js), so markup outside this wrapper gets the shop admin's Bootstrap
     * and nothing else. `canDarkMode` opts the pane into following the OS colour scheme when the
     * admin has not forced `theme-light`/`theme-dark`; both branches are configured as the
     * `darkMode` variant. Same contract as tnt_workflows, which is the point — the two plugins are
     * meant to look like one product.
     */
    protected function shell(string $html): string
    {
        return '<link href="' . htmlspecialchars($this->adminUrl() . 'css/tailwind.min.css', \ENT_QUOTES)
            . '" rel="stylesheet">'
            . '<div class="tailwind canDarkMode">' . $html . '</div>';
    }

    /** URL of this plugin's adminmenu/ directory, with a trailing slash. */
    protected function adminUrl(): string
    {
        try {
            return rtrim($this->plugin->getPaths()->getAdminURL(), '/') . '/';
        } catch (Throwable) {
            return '';
        }
    }

    protected function notLoggedInNotice(): void
    {
        echo '<div class="alert alert-warning">'
            . htmlspecialchars($this->text([
                'de' => 'Bitte als Backend-Administrator anmelden, um diese Ansicht zu sehen.',
                'en' => 'Please sign in as a backend administrator to see this view.',
            ]), \ENT_QUOTES)
            . '</div>';
    }
}
