<?php

final class IcadService
{
    private PDO $db;

    public function __construct(PDO $db)
    {
        $this->db = $db;
    }

    // ========= CACHE =========

    public function getCachedByAnimal(int $animalId): ?array
    {
        $st = $this->db->prepare('SELECT * FROM icad_cache WHERE animal_id=:id LIMIT 1');
        $st->execute([':id' => $animalId]);
        $row = $st->fetch(PDO::FETCH_ASSOC);
        if (!$row) {
            return null;
        }

        $data = $row['data_json'] ? json_decode($row['data_json'], true) : null;

        return [
            'animal_id' => (int) $row['animal_id'],
            'chip_id' => (string) ($row['chip_id'] ?? ''),
            'status' => (string) ($row['status'] ?? 'empty'), // ok|error|empty
            'error' => (string) ($row['error_msg'] ?? ''),
            'fetched_at' => (string) ($row['fetched_at'] ?? ''),
            'data' => is_array($data) ? $data : null,
        ];
    }

    public function isStale(?array $cache, int $ttlSeconds = 86400): bool
    {
        if (!$cache || empty($cache['fetched_at'])) {
            return true;
        }

        // si en erreur → on retente
        if (($cache['status'] ?? '') !== 'ok') {
            return true;
        }

        $t = strtotime($cache['fetched_at']);
        if ($t === false) {
            return true;
        }

        return time() - $t > $ttlSeconds;
    }

    private function saveCache(int $animalId, string $chipId, string $status, ?string $err, ?array $data): void
    {
        $st = $this->db->prepare("
        INSERT INTO icad_cache(animal_id, chip_id, data_json, fetched_at, status, error_msg)
        VALUES(:aid,:chip,:json,datetime('now'),:st,:err)
        ON CONFLICT(animal_id) DO UPDATE SET
        chip_id    = excluded.chip_id,
        data_json  = excluded.data_json,
        fetched_at = excluded.fetched_at,
        status     = excluded.status,
        error_msg  = excluded.error_msg
        ");
        $st->execute([
            ':aid' => $animalId,
            ':chip' => $chipId,
            ':json' => $data ? json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : null,
            ':st' => $status,
            ':err' => $err,
        ]);
    }

    // ========= PUBLIC API =========

    public function refreshByChip(int $animalId, string $chipId): array
    {
        $chipId = preg_replace('/\D+/', '', $chipId);
        if ($chipId === '') {
            throw new RuntimeException(t('service.icad.invalid_chip'));
        }

        $login = getenv('ICAD_LOGIN') ?: '';
        $pass = getenv('ICAD_PASSWORD') ?: '';
        if ($login === '' || $pass === '') {
            throw new RuntimeException(t('service.icad.credentials_missing'));
        }

        $cookie = sys_get_temp_dir() . '/twix_icad_' . bin2hex(random_bytes(6)) . '.cookie';

        try {
            // 1) GET HOME => token _token du formulaire login ACTEUR
            $r1 = $this->req('https://www.i-cad.fr/', $cookie);

            $token = $this->extractToken($r1['body'], '_token'); // prend le 1er _token trouvé

            // 2) POST login/check/acteur (XHR)
            $post = http_build_query([
                '_token' => $token,
                'login' => $login,
                'password' => $pass,
            ]);

            $r2 = $this->req('https://www.i-cad.fr/login/check/acteur', $cookie, 'POST', $post, [
                'Accept: */*',
                'Content-Type: application/x-www-form-urlencoded; charset=UTF-8',
                'X-Requested-With: XMLHttpRequest',
                'Origin: https://www.i-cad.fr',
                'Referer: https://www.i-cad.fr/',
            ]);

            // 3) Vérif session
            $r2b = $this->req('https://www.i-cad.fr/pro', $cookie);
            if (stripos($r2b['body'], 'Se connecter') !== false) {
                throw new RuntimeException(t('service.icad.login_failed'));
            }

            // 4) GET recherche. I-CAD a renommé le formulaire
            // search_animal en animal_search en 2026 : on accepte les deux.
            $r3 = $this->req('https://www.i-cad.fr/identification/recherche', $cookie);
            [$searchForm, $searchToken] = $this->extractAnimalSearchForm($r3['body']);

            // 5) POST recherche
            $post2 = http_build_query([
                $searchForm . '[insert]' => $chipId,
                $searchForm . '[tatou]' => '',
                $searchForm . '[espece]' => '',
                $searchForm . '[sexe]' => '',
                $searchForm . '[robe]' => '',
                $searchForm . '[_token]' => $searchToken,
            ]);

            $r4 = $this->req('https://www.i-cad.fr/identification/recherche', $cookie, 'POST', $post2, [
                'Content-Type: application/x-www-form-urlencoded',
                'Origin: https://www.i-cad.fr',
                'Referer: https://www.i-cad.fr/identification/recherche',
            ]);

            $animalPath = $this->extractAnimalPath($r4);
            if (!$animalPath) {
                throw new RuntimeException(t('service.icad.no_result'));
            }

            // 6) GET fiche animal
            $r5 = $this->req('https://www.i-cad.fr' . $animalPath, $cookie);
            $data = $this->parseAnimalHtml($r5['body'], $animalPath);

            $this->saveCache($animalId, $chipId, 'ok', null, $data);
            return $data;
        } catch (Throwable $e) {
            $this->saveCache($animalId, $chipId, 'error', $e->getMessage(), null);
            throw $e;
        } finally {
            @unlink($cookie);
        }
    }

    // ========= HTTP =========

    private function req(
        string $url,
        string $cookieFile,
        string $method = 'GET',
        ?string $body = null,
        array $headers = [],
    ): array {
        $ch = curl_init($url);

        $defaultHeaders = [
            'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
            'Accept-Language: fr,en-US;q=0.9,en;q=0.8',
            'Connection: keep-alive',
            'DNT: 1',
            'Sec-GPC: 1',
        ];
        $allHeaders = array_merge($defaultHeaders, $headers);

        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_MAXREDIRS => 10,
            CURLOPT_HEADER => true,
            CURLOPT_COOKIEJAR => $cookieFile,
            CURLOPT_COOKIEFILE => $cookieFile,
            CURLOPT_USERAGENT => 'Mozilla/5.0 (X11; Linux x86_64; rv:147.0) Gecko/20100101 Firefox/147.0',
            CURLOPT_TIMEOUT => 25,
            CURLOPT_CONNECTTIMEOUT => 10,
            CURLOPT_ENCODING => '', // gzip/br si dispo
            CURLOPT_HTTPHEADER => $allHeaders,
        ]);

        if ($method === 'POST') {
            curl_setopt($ch, CURLOPT_POST, true);
            curl_setopt($ch, CURLOPT_POSTFIELDS, $body ?? '');
        }

        $raw = curl_exec($ch);
        if ($raw === false) {
            $err = curl_error($ch);
            curl_close($ch);
            throw new RuntimeException(t('service.icad.network_error', ['error' => $err]));
        }

        $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $hs = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
        $effUrl = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
        curl_close($ch);

        return [
            'status' => $status,
            'headers' => substr($raw, 0, $hs),
            'body' => substr($raw, $hs),
            'effective_url' => $effUrl,
        ];
    }

    // ========= TOKENS / PARSING =========

    private function extractToken(string $html, string $fieldName): string
    {
        $field = preg_quote($fieldName, '/');

        // 1) cas standard: name="X" value="Y"
        $re = '/<input\b[^>]*\bname=(["\'])' . $field . '\1[^>]*\bvalue=(["\'])(.*?)\2[^>]*>/is';
        if (preg_match($re, $html, $m)) {
            return html_entity_decode($m[3], ENT_QUOTES);
        }

        // 2) fallback: value avant name
        $re2 = '/<input\b[^>]*\bvalue=(["\'])(.*?)\1[^>]*\bname=(["\'])' . $field . '\3[^>]*>/is';
        if (preg_match($re2, $html, $m)) {
            return html_entity_decode($m[2], ENT_QUOTES);
        }

        // 3) si fieldName == _token : on prend le 1er _token trouvé
        if ($fieldName === '_token') {
            if (preg_match('/<input\b[^>]*\bname=(["\'])_token\1[^>]*\bvalue=(["\'])(.*?)\2[^>]*>/is', $html, $m)) {
                return html_entity_decode($m[3], ENT_QUOTES);
            }
        }

        throw new RuntimeException(t('service.icad.token_missing', ['field' => $fieldName]));
    }

    private function extractAnimalSearchForm(string $html): array
    {
        foreach (['animal_search', 'search_animal'] as $formName) {
            try {
                return [$formName, $this->extractToken($html, $formName . '[_token]')];
            } catch (RuntimeException $e) {
                // Essayer le nom historique suivant.
            }
        }

        throw new RuntimeException(t('service.icad.form_missing'));
    }

    private function extractAnimalPath(array $resp): ?string
    {
        // souvent une redirection finale /animal/XXXX dans effective_url
        $p = parse_url($resp['effective_url'] ?? '', PHP_URL_PATH);
        if (is_string($p) && preg_match('~^/animal/\d+~', $p)) {
            return $p;
        }

        // fallback HTML
        if (preg_match('~href=["\'](/animal/\d+)["\']~', $resp['body'], $m)) {
            return $m[1];
        }
        if (preg_match('~(/animal/\d+)~', $resp['body'], $m)) {
            return $m[1];
        }
        return null;
    }

    private function parseAnimalHtml(string $html, string $animalPath): array
    {
        libxml_use_internal_errors(true);

        $dom = new DOMDocument();
        $dom->loadHTML($html);
        $xp = new DOMXPath($dom);

        $txt = function (string $xpath) use ($xp): string {
            $n = $xp->query($xpath);
            if (!$n || $n->length === 0) {
                return '';
            }
            return trim(preg_replace('~\s+~', ' ', $n->item(0)->textContent));
        };

        $name = $txt('//*[@id="animal_informations"]//*[contains(@class,"title")]//span[contains(@class,"emphasis")]');
        $insertRaw = $txt('//*[@id="animal_informations"]//*[contains(@class,"id")]//span[contains(@class,"number")]');
        $insert = preg_replace('~\s+~', '', $insertRaw);

        // infos label/value
        $rows = $xp->query('//*[@id="animal_informations"]//*[contains(@class,"table-row")]');
        $map = [];
        foreach ($rows as $row) {
            $ln = $xp->query('.//*[contains(@class,"label")]', $row)->item(0);
            $vn = $xp->query('.//*[contains(@class,"value")]', $row)->item(0);
            if (!$ln || !$vn) {
                continue;
            }
            $label = trim(preg_replace('~\s+~', ' ', $ln->textContent));
            $value = trim(preg_replace('~\s+~', ' ', $vn->textContent));
            if ($label !== '') {
                $map[$label] = $value;
            }
        }

        // events
        $events = [];
        $trs = $xp->query('//*[contains(@class,"event-row")]');
        foreach ($trs as $tr) {
            $tds = $xp->query('./td', $tr);
            if (!$tds || $tds->length < 3) {
                continue;
            }
            $events[] = [
                'date' => trim($tds->item(0)->textContent),
                'label' => trim(preg_replace('~\s+~', ' ', $tds->item(1)->textContent)),
                'recorded_at' => trim($tds->item(2)->textContent),
            ];
        }

        // Coordonnées du détenteur.
        $ownerName = $txt('//*[@id="down_right_block"]//*[contains(@class,"coords")]//*[contains(@class,"name")]');
        $ownerEmail = $txt('//*[@id="down_right_block"]//*[contains(@class,"coords")]//*[contains(@class,"address")]');
        $ownerPhone = $txt(
            '//*[@id="down_right_block"]//*[contains(@class,"coords")]//*[contains(@class,"phone_numbers")]',
        );
        $ownerPhone = preg_replace('~\s+~', ' ', $ownerPhone);

        // Adresse postale: i-CAD ne la met pas toujours dans un bloc unique, mais on tente
        $ownerPostal = $txt('//*[@id="down_right_block"]//*[contains(@class,"coords")]//*[contains(@class,"postal")]');
        if ($ownerPostal === '') {
            // fallback: parfois l’adresse est dans le même bloc coords mais pas taggée
            $coordsText = $txt('//*[@id="down_right_block"]//*[contains(@class,"coords")]');
            // on évite de tout recracher, juste si on détecte des numéros/rue
            $ownerPostal = $coordsText;
        }

        return [
            'icad_url' => 'https://www.i-cad.fr' . $animalPath,
            'name' => $name,
            'insert' => $insert,
            'species' => $map['Espèce'] ?? '',
            'sex' => $map['Sexe'] ?? '',
            'birth_date' => $map['Né le'] ?? '',
            'coat_type' => $map['Type de robe'] ?? '',
            'appearance' => $map['Apparence raciale'] ?? '',
            'events' => $events,
            'owner' => [
                'name' => $ownerName,
                'phone' => $ownerPhone,
                'email' => $ownerEmail,
                'postal' => $ownerPostal,
            ],
        ];
    }
}
