<?php
/**
 * Undetectable.php
 *
 * Thin client for Undetectable.ai Humanizer + AI Detector APIs.
 * Docs: https://help.undetectable.ai
 *
 * Auth: define the UNDETECTABLE_API_KEY constant where this client is used
 *        (e.g. in the cron script), or pass the key to the constructor.
 *
 * Usage:
 *   $ud        = new Undetectable();
 *   $humanHtml = $ud->humanizeHtml($contentHtml);  // humanizes <p>/<li> prose, keeps headings
 *   $score     = $ud->detect($humanHtml);          // ['result'=>0-100 AI%, 'human'=>0-100]
 */
class Undetectable {

    const HUMANIZE_URL = 'https://humanize.undetectable.ai';
    const DETECT_URL   = 'https://ai-detect.undetectable.ai';
    const MIN_CHARS    = 50; // humanizer minimum input length

    private $apiKey;

    public function __construct($apiKey = null) {
        $this->apiKey = $apiKey ?: (defined('UNDETECTABLE_API_KEY') ? UNDETECTABLE_API_KEY : '');
    }

    /**
     * Humanize a plain-text block (submit -> poll /document).
     * Returns the humanized text, or the ORIGINAL text on failure/timeout
     * (never null, so callers can never blank out content).
     */
    public function humanize($text, $opts = array(), $maxWaitSec = 120) {
        $text = (string) $text;
        if (strlen(trim($text)) < self::MIN_CHARS) {
            return $text;
        }

        $payload = array(
            'content'     => $text,
            'readability' => isset($opts['readability']) ? $opts['readability'] : 'University',
            'purpose'     => isset($opts['purpose'])     ? $opts['purpose']     : 'Article',
            // "More Human" + v11 measured ~100% -> ~7% AI in testing; "Balanced"/v2
            // left it at ~100%. Evasion is the goal, so default to the strong combo.
            'strength'    => isset($opts['strength'])    ? $opts['strength']    : 'More Human',
            'model'       => isset($opts['model'])       ? $opts['model']       : 'v11',
        );

        $sub = $this->request(self::HUMANIZE_URL . '/submit', $payload, true);
        $id  = isset($sub['id']) ? $sub['id'] : null;
        if (!$id) {
            return $text;
        }

        $waited = 0;
        while ($waited < $maxWaitSec) {
            sleep(5);
            $waited += 5;
            $doc = $this->request(self::HUMANIZE_URL . '/document', array('id' => $id), true);
            if (!empty($doc['output'])) {
                return $doc['output'];
            }
        }
        return $text;
    }

    /**
     * Run AI detection on text (submit -> poll /query).
     * HTML is stripped first; ~200+ words recommended for accuracy.
     * Returns array('result'=>float 0-100 AI-likelihood, 'human'=>float) or null.
     */
    public function detect($text) {
        $text = trim(strip_tags((string) $text));
        if ($text === '') {
            return null;
        }

        $payload = array(
            'text'        => $text,
            'key'         => $this->apiKey,
            'model'       => 'xlm_ud_detector',
            'retry_count' => 0,
        );

        $sub = $this->request(self::DETECT_URL . '/detect', $payload, false);
        $id  = isset($sub['id']) ? $sub['id'] : null;
        if (!$id) {
            return null;
        }

        for ($i = 0; $i < 15; $i++) {
            sleep(2);
            $q = $this->request(self::DETECT_URL . '/query', array('id' => $id), false);
            if (isset($q['status']) && $q['status'] === 'done') {
                return array(
                    'result' => isset($q['result']) ? (float) $q['result'] : null,
                    'human'  => isset($q['result_details']['human']) ? (float) $q['result_details']['human'] : null,
                );
            }
        }
        return null;
    }

    /**
     * Humanize the prose of an HTML article (<p> and <li> blocks only),
     * leaving headings and other markup intact so SEO keywords in
     * H1/H2/H3 are preserved.
     */
    public function humanizeHtml($html, $opts = array()) {
        $self = $this;
        return preg_replace_callback(
            '/<(p|li)\b([^>]*)>(.*?)<\/\1>/is',
            function ($m) use ($self, $opts) {
                $plain = trim(strip_tags($m[3]));
                if (strlen($plain) < Undetectable::MIN_CHARS) {
                    return $m[0];
                }
                $human = $self->humanize($plain, $opts);
                return '<' . $m[1] . $m[2] . '>' . $human . '</' . $m[1] . '>';
            },
            $html
        );
    }

    /** Remaining word credits, or null on failure. */
    public function credits() {
        $res = $this->request(self::DETECT_URL . '/check-user-credits', null, true, 'GET');
        return isset($res['credits']) ? (float) $res['credits'] : null;
    }

    /**
     * @param bool $headerAuth true => send apikey header; false => key is in the body.
     */
    private function request($url, $payload, $headerAuth, $method = 'POST') {
        $ch = curl_init();
        $headers = array('Content-Type: application/json', 'accept: application/json');
        if ($headerAuth && $this->apiKey) {
            $headers[] = 'apikey: ' . $this->apiKey;
        }
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($ch, CURLOPT_TIMEOUT, 120);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        if (strtoupper($method) === 'POST') {
            $json = json_encode($payload);
            if ($json === false && is_array($payload)) {
                // Content stored as latin1 (e.g. smart quotes) is invalid UTF-8 and
                // makes json_encode return false -> empty body -> silent API no-op.
                // Coerce strings to UTF-8 so the call actually goes through.
                array_walk_recursive($payload, function (&$v) {
                    if (is_string($v) && !mb_check_encoding($v, 'UTF-8')) {
                        $v = mb_convert_encoding($v, 'UTF-8', 'ISO-8859-1');
                    }
                });
                $json = json_encode($payload);
            }
            curl_setopt($ch, CURLOPT_POST, true);
            curl_setopt($ch, CURLOPT_POSTFIELDS, $json);
        }
        $resp = curl_exec($ch);
        curl_close($ch);
        if ($resp === false) {
            return array();
        }
        $data = json_decode($resp, true);
        return is_array($data) ? $data : array();
    }
}
