<?php

declare(strict_types=1);

namespace Plugin\tnt_mcp\src\Mcp\Commerce;

use JTL\DB\DbInterface;

/**
 * The payment-method domain object — the `tzahlungsart` row plus the three things that are NOT in
 * it and that a plain `SELECT * FROM tzahlungsart` therefore reports as absent:
 *
 *  - **its module settings**, which live in `teinstellungen` keyed by name and are *declared* in
 *    `teinstellungenconf WHERE cModulId = <the method's cModulId>` — a different set per module,
 *    which is exactly why the action catalog cannot enumerate them and this reader must.
 *  - **its per-language texts** (`tzahlungsartsprache`), one row per active language, which a save
 *    rewrites wholesale.
 *  - **which shipping methods offer it**, and the surcharge each one adds
 *    (`tversandartzahlungsart`) — the mirror image of {@see ShippingMethodReader}.
 *
 * Two traps are decoded rather than passed through raw, because both are silently misread
 * otherwise:
 *
 *  - `nMailSenden` is a **bitmask**, not a flag: `ZAHLUNGSART_MAIL_EINGANG` (0x1) and
 *    `ZAHLUNGSART_MAIL_STORNO` (0x10) (`includes/defines_inc.php:115-116`). A save rebuilds it from
 *    both POST keys, so reading it as one boolean is how the other half gets cleared.
 *  - `cKundengruppen` means "all groups" when NULL **or empty**, unlike `tversandart`'s `-1`
 *    (`Shipping/Repositories/DatabaseRepository.php:322-324`).
 *
 * `cModulId` is not unique: two `tzahlungsart` rows may share a module, and then they share one
 * settings block and one payment log. Every result says how many rows the module has, so a caller
 * cannot mistake "this method's settings" for something method-scoped.
 *
 * Secret-looking setting values are redacted (D32's rule, applied at the source here rather than
 * hoping a caller notices): a payment module's settings are the one place in this plugin's read
 * surface where a live API key or password plausibly sits.
 */
final class PaymentMethodReader
{
    /** `includes/defines_inc.php:115` */
    private const MAIL_INCOMING = 0x0001;

    /** `includes/defines_inc.php:116` */
    private const MAIL_CANCELLATION = 0x0010;

    /** Value keys whose content never belongs in an MCP answer. */
    private const SECRET = '/(pass|token|secret|hash|apikey|api_key|schluessel|signature)/i';

    private const REDACTED = '***';

    public function __construct(private readonly DbInterface $db)
    {
    }

    /**
     * Every payment method, one compact row each.
     *
     * @return array<string,mixed>
     */
    public function methods(): array
    {
        $rows = $this->db->getObjects(
            'SELECT za.kZahlungsart, za.cName, za.cModulId, za.nActive, za.nNutzbar, za.nSort,
                    za.cKundengruppen, za.cAnbieter, za.nWaehrendBestellung, za.nMailSenden,
                    (SELECT COUNT(*) FROM tversandartzahlungsart AS vz
                      WHERE vz.kZahlungsart = za.kZahlungsart) AS shippingMethods
               FROM tzahlungsart AS za
              ORDER BY za.nSort, za.kZahlungsart'
        );

        $methods = [];
        foreach ($rows as $row) {
            $methods[] = [
                'paymentMethodId' => (int)$row->kZahlungsart,
                'name'            => (string)$row->cName,
                'moduleId'        => (string)($row->cModulId ?? ''),
                'provider'        => (string)($row->cAnbieter ?? ''),
                'active'          => (int)$row->nActive === 1,
                // Derived, never posted: PHP SOAP/cURL/socket support decides it
                // (`Helpers/PaymentMethod.php:246-257`).
                'usable'          => (int)$row->nNutzbar === 1,
                'sort'            => (int)$row->nSort,
                'customerGroupIds' => self::customerGroups($row->cKundengruppen),
                'duringCheckout'  => (int)$row->nWaehrendBestellung === 1,
                'shippingMethods' => (int)$row->shippingMethods,
            ];
        }

        return [
            'total'   => \count($methods),
            'methods' => $methods,
            'note'    => 'Call get_payment_method for module settings, per-language texts and '
                . 'the shipping methods that offer it.',
        ];
    }

    /**
     * One payment method in full, or null when nothing matches.
     *
     * @param int|string $idOrName internal id, or the exact `cName` / `cModulId`
     *
     * @return array<string,mixed>|null
     */
    public function method(int|string $idOrName): ?array
    {
        $base = \is_int($idOrName)
            ? $this->db->getSingleObject(
                'SELECT * FROM tzahlungsart WHERE kZahlungsart = :id',
                ['id' => $idOrName]
            )
            : $this->db->getSingleObject(
                'SELECT * FROM tzahlungsart
                  WHERE cName = :name OR cModulId = :name
                  ORDER BY kZahlungsart
                  LIMIT 1',
                ['name' => $idOrName]
            );
        if ($base === null) {
            return null;
        }
        $id       = (int)$base->kZahlungsart;
        $moduleID = (string)($base->cModulId ?? '');
        $mailBits = (int)($base->nMailSenden ?? 0);

        return [
            'paymentMethodId' => $id,
            'name'            => (string)$base->cName,
            'moduleId'        => $moduleID,
            'provider'        => (string)($base->cAnbieter ?? ''),
            'active'          => (int)($base->nActive ?? 0) === 1,
            'usable'          => (int)($base->nNutzbar ?? 0) === 1,
            'sort'            => (int)($base->nSort ?? 0),
            'image'           => (string)($base->cBild ?? ''),
            'duringCheckout'  => (int)($base->nWaehrendBestellung ?? 0) === 1,
            'tsCode'          => (string)($base->cTSCode ?? ''),
            'templates'       => [
                'additionalStep' => (string)($base->cZusatzschrittTemplate ?? ''),
                'plugin'         => (string)($base->cPluginTemplate ?? ''),
            ],
            'requirements'    => [
                'curl'    => (int)($base->nCURL ?? 0) === 1,
                'soap'    => (int)($base->nSOAP ?? 0) === 1,
                'sockets' => (int)($base->nSOCKETS ?? 0) === 1,
            ],
            // Both halves, always. Sending one back without the other on a save clears the other.
            'mails'           => [
                'raw'          => $mailBits,
                'onIncoming'   => ($mailBits & self::MAIL_INCOMING) !== 0,
                'onCancelled'  => ($mailBits & self::MAIL_CANCELLATION) !== 0,
            ],
            'customerGroups'  => $this->customerGroupDetails($base->cKundengruppen),
            'languages'       => $this->languages($id),
            'settings'        => $this->settings($moduleID),
            'shippingMethods' => $this->shippingMethods($id),
            'moduleShared'    => $this->moduleSiblings($moduleID, $id),
        ];
    }

    /**
     * The module's own settings: every declared key (`teinstellungenconf`) joined to its stored
     * value (`teinstellungen`), so a key that has never been written shows up as declared with a
     * null value instead of vanishing. Group headers (`cConf = 'N'`) are skipped — they are
     * captions, not settings.
     *
     * @return list<array<string,mixed>>
     */
    private function settings(string $moduleID): array
    {
        if ($moduleID === '') {
            return [];
        }
        $rows = $this->db->getObjects(
            "SELECT c.kEinstellungenConf, c.cWertName, c.cName, c.cInputTyp,
                    c.kEinstellungenSektion, e.cWert
               FROM teinstellungenconf AS c
               LEFT JOIN teinstellungen AS e
                      ON e.cName = c.cWertName
                     AND e.kEinstellungenSektion = c.kEinstellungenSektion
              WHERE c.cModulId = :module AND c.cConf = 'Y'
              ORDER BY c.nSort, c.kEinstellungenConf",
            ['module' => $moduleID]
        );

        $settings = [];
        foreach ($rows as $row) {
            $key        = (string)$row->cWertName;
            $value      = $row->cWert === null ? null : (string)$row->cWert;
            $secret     = \preg_match(self::SECRET, $key) === 1;
            $settings[] = [
                'key'       => $key,
                'label'     => (string)($row->cName ?? ''),
                'inputType' => (string)($row->cInputTyp ?? ''),
                'section'   => (int)$row->kEinstellungenSektion,
                'value'     => $secret && $value !== null && $value !== '' ? self::REDACTED : $value,
                'redacted'  => $secret && $value !== null && $value !== '',
                'options'   => $this->options((int)$row->kEinstellungenConf, (string)($row->cInputTyp ?? '')),
            ];
        }

        return $settings;
    }

    /**
     * Allowed values of a selectbox setting. Anything else has none, and asking would be a query
     * per setting for an empty result.
     *
     * @return list<array<string,string>>
     */
    private function options(int $confID, string $inputType): array
    {
        if ($inputType !== 'selectbox') {
            return [];
        }
        $rows    = $this->db->getObjects(
            'SELECT cName, cWert FROM teinstellungenconfwerte
              WHERE kEinstellungenConf = :id
              ORDER BY nSort',
            ['id' => $confID]
        );
        $options = [];
        foreach ($rows as $row) {
            $options[] = ['value' => (string)$row->cWert, 'label' => (string)$row->cName];
        }

        return $options;
    }

    /**
     * The four per-language fields a `payment.method.saveConfig` rewrites, keyed by language code.
     *
     * @return array<string,array<string,string>>
     */
    private function languages(int $id): array
    {
        $rows = $this->db->getObjects(
            'SELECT cISOSprache, cName, cGebuehrname, cHinweisText, cHinweisTextShop
               FROM tzahlungsartsprache
              WHERE kZahlungsart = :id',
            ['id' => $id]
        );

        $languages = [];
        foreach ($rows as $row) {
            $languages[(string)$row->cISOSprache] = [
                'name'     => (string)($row->cName ?? ''),
                'feeName'  => (string)($row->cGebuehrname ?? ''),
                'note'     => (string)($row->cHinweisText ?? ''),
                'noteShop' => (string)($row->cHinweisTextShop ?? ''),
            ];
        }

        return $languages;
    }

    /**
     * Which shipping methods offer this payment method, and what each adds on top.
     *
     * @return list<array<string,mixed>>
     */
    private function shippingMethods(int $id): array
    {
        $rows = $this->db->getObjects(
            'SELECT vz.kVersandart, vz.fAufpreis, vz.cAufpreisTyp, va.cName
               FROM tversandartzahlungsart AS vz
               LEFT JOIN tversandart AS va ON va.kVersandart = vz.kVersandart
              WHERE vz.kZahlungsart = :id
              ORDER BY va.nSort, vz.kVersandart',
            ['id' => $id]
        );

        $methods = [];
        foreach ($rows as $row) {
            $type      = (string)($row->cAufpreisTyp ?? '');
            $methods[] = [
                'shippingMethodId' => (int)$row->kVersandart,
                'name'             => (string)($row->cName ?? ''),
                'surcharge'        => \round((float)$row->fAufpreis, 2),
                'surchargeType'    => $type === 'prozent' ? 'percent' : 'absolute',
                'surchargeTypeRaw' => $type,
            ];
        }

        return $methods;
    }

    /**
     * Other `tzahlungsart` rows on the same module — they share these settings and one payment log,
     * so `payment.method.logReset` on any of them clears the log of all of them.
     *
     * @return array<string,mixed>
     */
    private function moduleSiblings(string $moduleID, int $id): array
    {
        if ($moduleID === '') {
            return ['sharedWith' => [], 'note' => 'This method has no module id.'];
        }
        $rows   = $this->db->getObjects(
            'SELECT kZahlungsart, cName FROM tzahlungsart
              WHERE cModulId = :module AND kZahlungsart <> :id
              ORDER BY kZahlungsart',
            ['module' => $moduleID, 'id' => $id]
        );
        $shared = [];
        foreach ($rows as $row) {
            $shared[] = ['paymentMethodId' => (int)$row->kZahlungsart, 'name' => (string)$row->cName];
        }

        return [
            'sharedWith' => $shared,
            'note'       => $shared === []
                ? 'No other method uses this module id.'
                : 'These methods share this module\'s settings and its payment log.',
        ];
    }

    /**
     * @return array<string,mixed>
     */
    private function customerGroupDetails(?string $packed): array
    {
        $ids = self::customerGroups($packed);
        if ($ids === []) {
            return ['restricted' => false, 'groups' => []];
        }
        $params = [];
        $names  = [];
        foreach ($ids as $i => $groupID) {
            $params['v' . $i] = $groupID;
            $names[]          = ':v' . $i;
        }
        $rows   = $this->db->getObjects(
            'SELECT kKundengruppe, cName FROM tkundengruppe
              WHERE kKundengruppe IN (' . \implode(', ', $names) . ')',
            $params
        );
        $groups = [];
        foreach ($rows as $row) {
            $groups[] = ['customerGroupId' => (int)$row->kKundengruppe, 'name' => (string)$row->cName];
        }

        return ['restricted' => true, 'groups' => $groups];
    }

    /**
     * NULL or empty means "every customer group" here — no `-1` sentinel, unlike `tversandart`
     * (`Shipping/Repositories/DatabaseRepository.php:322-324`).
     *
     * @return list<int>
     */
    private static function customerGroups(?string $packed): array
    {
        $raw = \trim((string)$packed);
        if ($raw === '' || $raw === '-1') {
            return [];
        }

        $ids = [];
        foreach (\preg_split('/[;,]/', $raw, -1, \PREG_SPLIT_NO_EMPTY) ?: [] as $part) {
            $part = \trim($part);
            if ($part !== '' && \ctype_digit($part)) {
                $ids[] = (int)$part;
            }
        }

        return $ids;
    }
}
