<?php
if (!defined('_PS_VERSION_')) {
    exit;
}

class KpiSkimmerCron extends Module
{
    const CONF_EMAIL = 'KPISKIMMER_EMAIL';
    const CONF_TOKEN = 'KPISKIMMER_TOKEN';
    const CONF_COOLDOWN_MIN = 'KPISKIMMER_COOLDOWN_MIN';
    const CONF_LAST_ALERT_TS = 'KPISKIMMER_LAST_ALERT_TS';
    const CONF_LAST_HASH = 'KPISKIMMER_LAST_HASH';

    public function __construct()
    {
        $this->name = 'kpiskimmercron';
        $this->tab = 'administration';
        $this->version = '1.0.0';
        $this->author = 'KPICREATION';
        $this->need_instance = 0;

        $this->bootstrap = true;

        parent::__construct();

        $this->displayName = $this->l('Security monitor – Skimmer checker');
        $this->description = $this->l('Checks active theme head.tpl for known digital skimmer patterns and alerts by email via a secured cron endpoint.');
    }

    public function install()
    {
        if (!parent::install()) {
            return false;
        }

        if (!Configuration::get(self::CONF_TOKEN)) {
            Configuration::updateValue(self::CONF_TOKEN, Tools::passwdGen(32));
        }

        if (!Configuration::get(self::CONF_EMAIL)) {
            Configuration::updateValue(self::CONF_EMAIL, Configuration::get('PS_SHOP_EMAIL'));
        }

        if (!Configuration::get(self::CONF_COOLDOWN_MIN)) {
            Configuration::updateValue(self::CONF_COOLDOWN_MIN, 360); // 6h
        }

        Configuration::updateValue(self::CONF_LAST_ALERT_TS, 0);
        Configuration::updateValue(self::CONF_LAST_HASH, '');

        return true;
    }

    public function uninstall()
    {
        Configuration::deleteByName(self::CONF_EMAIL);
        Configuration::deleteByName(self::CONF_TOKEN);
        Configuration::deleteByName(self::CONF_COOLDOWN_MIN);
        Configuration::deleteByName(self::CONF_LAST_ALERT_TS);
        Configuration::deleteByName(self::CONF_LAST_HASH);

        return parent::uninstall();
    }

    public function getContent()
    {
        $output = '';

        if (Tools::isSubmit('submitKpiSkimmerCron')) {
            $email = trim((string)Tools::getValue('KPISKIMMER_EMAIL'));
            $cooldown = (int)Tools::getValue('KPISKIMMER_COOLDOWN_MIN');

            if (!Validate::isEmail($email)) {
                $output .= $this->displayError($this->l('Invalid email address.'));
            } else {
                Configuration::updateValue(self::CONF_EMAIL, $email);
                Configuration::updateValue(self::CONF_COOLDOWN_MIN, max(0, $cooldown));
                $output .= $this->displayConfirmation($this->l('Settings updated.'));
            }
        }

        $token = Configuration::get(self::CONF_TOKEN);
        $cronUrl = Context::getContext()->link->getModuleLink($this->name, 'cron', ['token' => $token], true);

        $output .= '
        <div class="panel">
          <h3>'.$this->l('Cron URL').'</h3>
          <p>'.$this->l('Call this URL from your server cron (GET):').'</p>
          <p><code style="display:block;white-space:normal;word-break:break-all;">'.Tools::safeOutput($cronUrl).'</code></p>
          <p>'.$this->l('Tip: run hourly. The module includes a cooldown to avoid alert spam.').'</p>
        </div>';

        $output .= '
        <form method="post">
          <div class="panel">
            <h3>'.$this->l('Alert settings').'</h3>

            <div class="form-group">
              <label>'.$this->l('Alert email').'</label>
              <input type="email" name="KPISKIMMER_EMAIL" class="form-control" value="'.Tools::safeOutput(Configuration::get(self::CONF_EMAIL)).'" />
            </div>

            <div class="form-group">
              <label>'.$this->l('Cooldown (minutes)').'</label>
              <input type="number" name="KPISKIMMER_COOLDOWN_MIN" class="form-control" value="'.(int)Configuration::get(self::CONF_COOLDOWN_MIN).'" />
              <p class="help-block">'.$this->l('Minimum time between alerts when infection persists.').'</p>
            </div>

            <button type="submit" name="submitKpiSkimmerCron" class="btn btn-primary">
              '.$this->l('Save').'
            </button>
          </div>
        </form>';

        // Bouton test
        if (Tools::isSubmit('submitKpiSkimmerTest')) {
            $res = $this->runScanAndMaybeAlert(true);
            $output .= $res['ok']
                ? $this->displayConfirmation($this->l('Scan done. ').$res['message'])
                : $this->displayError($this->l('Scan failed: ').$res['message']);
        }

        $output .= '
        <form method="post">
          <div class="panel">
            <h3>'.$this->l('Manual test').'</h3>
            <button type="submit" name="submitKpiSkimmerTest" class="btn btn-default">
              '.$this->l('Run scan now').'
            </button>
          </div>
        </form>';

        return $output;
    }

    /**
     * Scan + alert.
     * @param bool $forceAlert If true, bypass cooldown (manual test).
     */
    public function runScanAndMaybeAlert($forceAlert = false)
    {
        try {
            $paths = $this->getTargetFiles();
            if (empty($paths)) {
                return ['ok' => true, 'infected' => false, 'message' => 'No head.tpl found for active theme.'];
            }

            $hits = [];
            foreach ($paths as $file) {
                if (!is_readable($file)) {
                    continue;
                }
                $content = Tools::file_get_contents($file);
                $match = $this->detectSkimmer($content);
                if ($match['infected']) {
                    $hits[] = [
                        'file' => $file,
                        'context' => $match['context'],
                        'hash' => sha1($content),
                    ];
                }
            }

            if (empty($hits)) {
                return ['ok' => true, 'infected' => false, 'message' => 'No suspicious pattern detected.'];
            }

            // Anti-spam
            $cooldownMin = (int)Configuration::get(self::CONF_COOLDOWN_MIN);
            $lastAlertTs = (int)Configuration::get(self::CONF_LAST_ALERT_TS);
            $now = time();

            $globalHash = sha1(json_encode(array_map(function ($h) {
                return $h['file'].'|'.$h['hash'];
            }, $hits)));

            $lastHash = (string)Configuration::get(self::CONF_LAST_HASH);

            $inCooldown = ($cooldownMin > 0) && ($now - $lastAlertTs < $cooldownMin * 60);
            $sameAsLast = ($globalHash === $lastHash);

            if (!$forceAlert && $inCooldown && $sameAsLast) {
                return ['ok' => true, 'infected' => true, 'message' => 'Infected, but cooldown active (no email sent).'];
            }

            $sent = $this->sendAlertEmail($hits);

            if ($sent) {
                Configuration::updateValue(self::CONF_LAST_ALERT_TS, $now);
                Configuration::updateValue(self::CONF_LAST_HASH, $globalHash);
                return ['ok' => true, 'infected' => true, 'message' => 'Infected. Alert email sent.'];
            }

            return ['ok' => false, 'infected' => true, 'message' => 'Infected. Email could not be sent (check mail config).'];

        } catch (Exception $e) {
            return ['ok' => false, 'infected' => false, 'message' => $e->getMessage()];
        }
    }

    private function getTargetFiles()
    {
        $theme = (string)Configuration::get('PS_THEME_NAME');
        $base = _PS_ROOT_DIR_.'/themes/'.$theme.'/templates/_partials/head.tpl';
        $paths = [];

        if (file_exists($base)) {
            $paths[] = $base;
        }

        // Optionnel: si vous avez des thèmes enfant/variantes, vous pouvez ajouter d’autres paths ici.

        return $paths;
    }

    private function detectSkimmer($content)
    {
        // On détecte la "structure" : XMLHttpRequest + atob( + Function(responseText)
        $p1 = '/XMLHttpRequest/i';
        $p2 = '/atob\s*\(/i';
        $p3 = '/Function\s*\([^)]*responseText/i';

        $infected = preg_match($p1, $content) && preg_match($p2, $content) && preg_match($p3, $content);

        // Contexte: on extrait des lignes autour des scripts (utile pour l’alerte)
        $context = '';
        if ($infected) {
            $lines = preg_split("/\r\n|\n|\r/", $content);
            $keep = [];
            foreach ($lines as $line) {
                if (preg_match('/<script|XMLHttpRequest|atob\s*\(|Function\s*\(/i', $line)) {
                    $keep[] = trim($line);
                }
                if (count($keep) >= 30) break;
            }
            $context = implode("\n", $keep);
        }

        return ['infected' => (bool)$infected, 'context' => $context];
    }

    private function sendAlertEmail(array $hits)
    {
        $to = (string)Configuration::get(self::CONF_EMAIL);
        if (!Validate::isEmail($to)) {
            return false;
        }

        $shopName = (string)Configuration::get('PS_SHOP_NAME');
        $subject = sprintf('[SECURITY][PrestaShop] Skimmer suspect détecté - %s', $shopName);

        $body = "Un pattern de digital skimmer a été détecté (XMLHttpRequest + atob() + Function(responseText)).\n\n";
        $body .= "Actions immédiates recommandées :\n";
        $body .= "- Mettre en maintenance la page de paiement si nécessaire\n";
        $body .= "- Comparer head.tpl avec une version saine (thème officiel)\n";
        $body .= "- Rechercher d’autres fichiers modifiés récemment\n";
        $body .= "- Changer les accès (BO/FTP/SSH/DB) et vérifier modules/thèmes\n\n";

        foreach ($hits as $h) {
            $body .= "Fichier: ".$h['file']."\n";
            $body .= "Extraits:\n".$h['context']."\n";
            $body .= "-----------------------------\n";
        }

        // Envoi simple sans template
        return (bool)Mail::Send(
            (int)Context::getContext()->language->id,
            'contact', // template existant (on le détourne), ou créez votre template dédié
            $subject,
            ['{message}' => nl2br($body)],
            $to,
            null,
            null,
            null,
            null,
            null,
            _PS_MAIL_DIR_,
            false,
            (int)Context::getContext()->shop->id
        );
    }

    public function getCronToken()
    {
        return (string)Configuration::get(self::CONF_TOKEN);
    }
}
