<?php

/**
 * Copyright 2024-2025 Simply.IN Sp. z o.o.
 *
 * NOTICE OF LICENSE
 *
 * Licensed under the EUPL-1.2 or later.
 * You may not use this work except in compliance with the Licence.
 *
 * Copy of the Licence is available at:
 * https://joinup.ec.europa.eu/software/page/eupl
 * It is bundled with this package in the file LICENSE.txt
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the Licence is distributed on an as is basis,
 * without warranties or conditions of any kind, either express or implied.
 * Check the Licence for the specific language governing permissions
 * and limitations under the License.
 *
 * @author   Simply.IN Sp. z o.o.
 * @copyright 2024-2025 Simply.IN Sp. z o.o.
 * @license   https://joinup.ec.europa.eu/software/page/eupl
 */

declare(strict_types=1);

namespace SimplyIn\Payment;

if (!defined('_PS_VERSION_')) {
    exit;
}

class PaymentLogoResolver
{
    private const BUNDLED_LOGOS = ['imoje.svg', 'p24.svg', 'payu.svg'];

    private const PROTECTED_FILES = ['index.php', 'imoje.svg', 'p24.svg', 'payu.svg'];

    private const DOWNLOADABLE_EXTENSIONS = ['png', 'gif', 'jpg', 'jpeg', 'webp'];

    private const NEGATIVE_CACHE_TTL = 3600;

    private string $logoDir;

    public function __construct(string $logoDir)
    {
        $this->logoDir = rtrim($logoDir, '/') . '/';
    }

    /**
     * @param iterable<object> $methods
     */
    public function resolve(iterable $methods): ?string
    {
        foreach ($methods as $method) {
            $url = $method->paymentProvider->logoUrl ?? null;
            if (!is_string($url) || $url === '') {
                continue;
            }

            $bundled = $this->bundledLogo($url);
            if ($bundled !== null) {
                return 'providers/' . $bundled;
            }

            $cached = $this->cachedRaster($url);
            if ($cached !== null) {
                return 'providers/' . $cached;
            }
        }

        return $this->fallbackByProviderName($methods);
    }

    /**
     * Providers without a usable logo still get the bundled artwork the front-end JS
     * (`addIcon`) used to inject — served via `PaymentOption::setLogo()` instead.
     *
     * @param iterable<object> $methods
     */
    private function fallbackByProviderName(iterable $methods): string
    {
        foreach ($methods as $method) {
            $name = strtolower((string) ($method->paymentProvider->id ?? $method->paymentProvider->name ?? ''));
            if (str_contains($name, 'payu')) {
                return 'payu_simplyin.svg';
            }
            if (str_contains($name, 'paynow')) {
                return 'paynow_simplyin.png';
            }
        }

        return 'simplyin_app_logo.svg';
    }

    private function bundledLogo(string $url): ?string
    {
        $path = parse_url($url, PHP_URL_PATH);
        if (!is_string($path) || $path === '') {
            return null;
        }
        $name = basename($path);
        if (!in_array($name, self::BUNDLED_LOGOS, true)) {
            return null;
        }

        return file_exists($this->logoDir . $name) ? $name : null;
    }

    private function cachedRaster(string $url): ?string
    {
        $urlExt = strtolower((string) pathinfo((string) parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION));
        if (!in_array($urlExt, self::DOWNLOADABLE_EXTENSIONS, true)) {
            return null;
        }

        $hash = hash('sha256', $url);
        foreach (['png', 'gif', 'jpg', 'webp'] as $ext) {
            if (file_exists($this->logoDir . $hash . '.' . $ext)) {
                return $hash . '.' . $ext;
            }
        }

        $miss = $this->logoDir . $hash . '.miss';
        if (is_file($miss) && (time() - (int) @filemtime($miss)) < self::NEGATIVE_CACHE_TTL) {
            return null;
        }

        if (!$this->cacheIsWritable()) {
            return null;
        }

        return $this->download($url, $hash);
    }

    protected function download(string $url, string $hash): ?string
    {
        $bytes = $this->fetch($url);
        if ($bytes !== null) {
            $ext = $this->rasterExtension($bytes);
            if ($ext !== null && $this->writeAtomic($this->logoDir . $hash . '.' . $ext, $bytes)) {
                return $hash . '.' . $ext;
            }
        }

        $this->markMiss($this->logoDir . $hash . '.miss');

        return null;
    }

    protected function fetch(string $url): ?string
    {
        if (!str_starts_with(strtolower($url), 'https://')) {
            return null;
        }
        $ch = curl_init($url);
        if ($ch === false) {
            return null;
        }
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
        curl_setopt($ch, CURLOPT_MAXREDIRS, 3);
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
        curl_setopt($ch, CURLOPT_TIMEOUT, 10);
        curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTPS);
        curl_setopt($ch, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTPS);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
        curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0');
        $result = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($httpCode !== 200 || !is_string($result) || strlen($result) > 512 * 1024) {
            return null;
        }

        return $result;
    }

    protected function writeAtomic(string $target, string $bytes): bool
    {
        $tmp = @tempnam($this->logoDir, 'sinlogo_');
        if ($tmp === false) {
            return false;
        }
        if (@file_put_contents($tmp, $bytes) !== false && @rename($tmp, $target)) {
            @chmod($target, 0o644);

            return true;
        }
        @unlink($tmp);

        return false;
    }

    private function markMiss(string $marker): void
    {
        @touch($marker);
    }

    protected function cacheIsWritable(): bool
    {
        return is_writable($this->logoDir);
    }

    private function rasterExtension(string $bytes): ?string
    {
        if (strlen($bytes) < 12) {
            return null;
        }
        if (substr($bytes, 0, 8) === "\x89PNG\r\n\x1a\n") {
            return 'png';
        }
        if (substr($bytes, 0, 6) === 'GIF87a' || substr($bytes, 0, 6) === 'GIF89a') {
            return 'gif';
        }
        if (substr($bytes, 0, 3) === "\xff\xd8\xff") {
            return 'jpg';
        }
        if (substr($bytes, 0, 4) === 'RIFF' && substr($bytes, 8, 4) === 'WEBP') {
            return 'webp';
        }

        return null;
    }

    /**
     * Remove every file from the provider-logo cache except the bundled, trusted assets. The
     * purge is UNCONDITIONAL by design: an older, unsafe version cached files by the raw URL
     * basename with only a content sniff, so a PNG-header polyglot named `payload.php` could
     * survive a magic-byte check. Deleting everything non-bundled removes that whole class at
     * once; the content-addressed cache simply repopulates itself on the next render.
     *
     * @return int number of files removed
     */
    public static function purgeUntrustedCache(string $logoDir): int
    {
        $dir = rtrim($logoDir, '/') . '/';
        if (!is_dir($dir)) {
            return 0;
        }

        $removed = 0;
        foreach ((array) @scandir($dir) as $file) {
            if ($file === '.' || $file === '..' || in_array($file, self::PROTECTED_FILES, true)) {
                continue;
            }
            $path = $dir . $file;
            if (is_file($path) && @unlink($path)) {
                ++$removed;
            }
        }

        return $removed;
    }
}
