<?php

declare(strict_types=1);

final class DocumentsService
{
    private const ADOPTION_TEMPLATE = __DIR__ . '/../../resources/documents/CONTRAT ADOPTION V1.5.odt';
    private const TEMPLATES = [
        'adoption' => self::ADOPTION_TEMPLATE,
        'abandon' => __DIR__ . '/../../resources/documents/ABANDON V1.0.odt',
        'benevole' => __DIR__ . '/../../resources/documents/CONTRAT BENEVOLE V2.0.odt',
        'fa' => __DIR__ . '/../../resources/documents/PROPOSITION FA V1.1.odt',
        'pre_adoption' => __DIR__ . '/../../resources/documents/CERTIFICAT VISITE PRE-ADOPTION V1.1.odt',
    ];

    public static function adoptionTemplate(): string
    {
        if (!is_file(self::ADOPTION_TEMPLATE)) {
            throw new RuntimeException(t('service.document.adoption_missing'));
        }
        return self::ADOPTION_TEMPLATE;
    }

    public static function template(string $type): string
    {
        $path = self::TEMPLATES[$type] ?? null;
        if (!$path || !is_file($path)) {
            throw new RuntimeException(t('service.document.template_missing'));
        }
        return $path;
    }

    public static function generateAbandon(array $animal): string
    {
        return self::transformTemplate('abandon', 'abandon-' . self::safeName((string) $animal['name']), function (
            DOMXPath $xp,
        ) use ($animal): void {
            $sex = match ($animal['sex'] ?? 'U') {
                'M' => 'Mâle',
                'F' => 'Femelle',
                default => 'Non renseigné',
            };
            $birth = trim((string) ($animal['birth_date'] ?? ''));
            $age = '—';
            if ($birth !== '') {
                $born = new DateTimeImmutable($birth);
                $now = new DateTimeImmutable();
                $diff = $born->diff($now);
                $age = $diff->y > 0 ? $diff->y . ' an' . ($diff->y > 1 ? 's' : '') : $diff->m . ' mois';
            }
            $chip = trim((string) ($animal['chip'] ?: $animal['chip_id'] ?? ''));
            self::replaceParagraph($xp, 'Nom de l’animal', [
                ['Nom de l’animal : ', 'label'],
                [(string) $animal['name'], 'value'],
                ['tab'],
                ['Sexe : ', 'label'],
                [$sex, 'value'],
            ]);
            self::replaceParagraph($xp, 'Race :', [
                ['Race : ', 'label'],
                [(string) ($animal['breed'] ?? '—'), 'value'],
                ['tab'],
                ['Âge : ', 'label'],
                [$age, 'value'],
                ['break'],
                ['Vaccination : ', 'label'],
                ['À vérifier', 'value'],
                ['tab'],
                ['Vermifugation : ', 'label'],
                ['À vérifier', 'value'],
                ['break'],
                ['Numéro de tatouage ou puce électronique : ', 'label'],
                [$chip !== '' ? $chip : '—', 'value'],
            ]);
        });
    }

    public static function generateContact(string $type, array $contact): string
    {
        if (!in_array($type, ['benevole', 'fa'], true)) {
            throw new InvalidArgumentException(t('service.document.invalid_type'));
        }
        return self::transformTemplate($type, $type . '-' . self::safeName((string) $contact['name']), function (
            DOMXPath $xp,
        ) use ($type, $contact): void {
            $name = (string) $contact['name'];
            $address = (string) ($contact['address'] ?? '');
            $postal = (string) ($contact['postal_code'] ?? '');
            $city = (string) ($contact['city'] ?? '');
            $phone = (string) ($contact['phone'] ?? '');
            $email = (string) ($contact['email'] ?? '');
            if ($type === 'benevole') {
                self::replaceParagraph(
                    $xp,
                    'Nom :',
                    [['Nom : ', 'label'], [$name, 'value'], ['tab'], ['Prénom : ', 'label'], ['', 'value']],
                    0,
                );
                self::replaceParagraph($xp, 'Adresse :', [['Adresse : ', 'label'], [$address, 'value']], 0);
                self::replaceParagraph(
                    $xp,
                    'Code Postal :',
                    [['Code Postal : ', 'label'], [$postal, 'value'], ['tab'], ['Ville : ', 'label'], [$city, 'value']],
                    0,
                );
                self::replaceParagraph(
                    $xp,
                    'Numéro de téléphone',
                    [
                        ['Numéro de téléphone : ', 'label'],
                        [$phone, 'value'],
                        ['tab'],
                        ['Adresse e-mail : ', 'label'],
                        [$email, 'value'],
                    ],
                    0,
                );
            } else {
                self::replaceParagraph($xp, 'NOM / Prénom :', [['NOM / Prénom : ', 'label'], [$name, 'value']], 0);
                self::replaceParagraph($xp, 'Adresse :', [['Adresse : ', 'label'], [$address, 'value']], 0);
                self::replaceParagraph(
                    $xp,
                    'Code postal :',
                    [['Code postal : ', 'label'], [$postal, 'value'], ['tab'], ['Ville : ', 'label'], [$city, 'value']],
                    0,
                );
                self::replaceParagraph(
                    $xp,
                    'Téléphone :',
                    [
                        ['Téléphone : ', 'label'],
                        [$phone, 'value'],
                        ['tab'],
                        ['Adresse mail : ', 'label'],
                        [$email, 'value'],
                    ],
                    0,
                );
            }
        });
    }

    public static function generateAdoption(array $animal, ?array $adoption, ?array $vaccine): string
    {
        $dir = self::tempDir();
        $name = self::safeName((string) $animal['name']);
        $target = $dir . '/contrat-adoption-' . $name . '.odt';
        if (!copy(self::adoptionTemplate(), $target)) {
            throw new RuntimeException(t('service.document.copy_failed'));
        }

        $zip = new ZipArchive();
        if ($zip->open($target) !== true) {
            throw new RuntimeException(t('service.document.open_failed'));
        }
        $xml = $zip->getFromName('content.xml');
        if ($xml === false) {
            $zip->close();
            throw new RuntimeException(t('service.document.content_invalid'));
        }
        $dom = new DOMDocument();
        $dom->preserveWhiteSpace = true;
        if (!$dom->loadXML($xml)) {
            $zip->close();
            throw new RuntimeException(t('service.document.xml_invalid'));
        }
        $xp = new DOMXPath($dom);
        $xp->registerNamespace('text', 'urn:oasis:names:tc:opendocument:xmlns:text:1.0');
        $xp->registerNamespace('office', 'urn:oasis:names:tc:opendocument:xmlns:office:1.0');
        self::addGeneratedFieldStyles($dom, $xp);

        $date = static fn(?string $value): string => $value ? date('d/m/Y', strtotime($value)) : '';
        $sex = match ($animal['sex'] ?? 'U') {
            'M' => '☒ Mâle    ☐ Femelle',
            'F' => '☐ Mâle    ☒ Femelle',
            default => '☐ Mâle    ☐ Femelle',
        };
        $origin = trim((string) ($animal['breed'] ?? ''));
        $chip = trim((string) ($animal['chip'] ?: $animal['chip_id'] ?? ''));
        $testStatus =
            'FIV ' .
            self::testLabel((string) ($animal['fiv_status'] ?? 'unknown')) .
            ' / FeLV ' .
            self::testLabel((string) ($animal['felv_status'] ?? 'unknown'));
        $signatureDate = $date($adoption['adoption_date'] ?? date('Y-m-d'));

        self::setSpan($xp, 'T26', (string) $animal['name']);
        self::setLabeledSpan($xp, 'T28', 'Sexe : ', $sex);
        self::setSpan($xp, 'T31', ' ' . $date($animal['birth_date'] ?? null));
        self::setSpan($xp, 'T33', 'Robe : ', null, 'GeneratedField');
        self::setSpan($xp, 'T34', (string) ($animal['color'] ?? ''));
        self::setSpan($xp, 'T41', $origin !== '' ? $origin : '—');
        self::setSpan($xp, 'T42', '');
        self::setLabeledSpan($xp, 'T45', 'Puce électronique N° : ', $chip !== '' ? $chip : '—');
        self::setLabeledSpan(
            $xp,
            'T49',
            ' :  ',
            (int) ($animal['sterilized'] ?? 0) === 1
                ? '☒ Effectuée    ☐ À effectuer avant le :'
                : '☐ Effectuée    ☒ À effectuer avant le :',
        );
        self::setLabeledSpan($xp, 'T53', 'Date du test FIV-FELV : ', $testStatus, 'tab');
        self::setLabeledSpan($xp, 'T55', 'Date de la dernière vaccination : ', $date($vaccine['done_date'] ?? null));
        self::setLabeledSpan($xp, 'T56', 'Rappel à effectuer le : ', $date($vaccine['due_date'] ?? null), 'tab');

        self::setSpan($xp, 'T98', (string) ($adoption['adopter_name'] ?? ''));
        self::setSpan($xp, 'T101', (string) ($adoption['adopter_address'] ?? ''));
        self::setSpan($xp, 'T104', (string) ($adoption['adopter_postal_code'] ?? ''));
        self::setSpan($xp, 'T107', (string) ($adoption['adopter_city'] ?? ''));
        self::setSpan($xp, 'T110', '');
        self::setSpan($xp, 'T113', (string) ($adoption['adopter_phone'] ?? ''));
        self::setSpan($xp, 'T116', (string) ($adoption['adopter_email'] ?? ''));
        $signatureCity = AppSettings::get('association_city') ?: '—';
        self::setLabeledSpan($xp, 'T155', 'Fait à : ', $signatureCity, null, 'GeneratedSignature');
        self::setLabeledSpan($xp, 'T156', 'Le : ', $signatureDate, 'tab', 'GeneratedSignature');
        self::setLabeledSpan($xp, 'T247', 'Fait à : ', $signatureCity, null, 'GeneratedSignature');
        self::setLabeledSpan($xp, 'T253', 'Le : ', $signatureDate, 'tab', 'GeneratedSignature');
        self::setSpan($xp, 'T283', (string) $animal['name']);
        self::setLabeledSpan($xp, 'T367', 'Fait à : ', $signatureCity, null, 'GeneratedSignature');
        self::setLabeledSpan($xp, 'T373', 'Le : ', $signatureDate, 'tab', 'GeneratedSignature');
        foreach (
            [
                'T25',
                'T30',
                'T33',
                'T36',
                'T40',
                'T45',
                'T48',
                'T52',
                'T53',
                'T55',
                'T56',
                'T97',
                'T100',
                'T103',
                'T106',
                'T109',
                'T112',
                'T115',
                'T281',
            ]
            as $labelStyle
        ) {
            self::applySpanStyle($xp, $labelStyle);
        }

        $updated = $dom->saveXML();
        if ($updated === false || !$zip->addFromString('content.xml', $updated)) {
            $zip->close();
            throw new RuntimeException(t('service.document.update_contract_failed'));
        }
        $zip->close();
        return $target;
    }

    public static function toPdf(string $odtPath): string
    {
        $dir = dirname($odtPath);
        $profile = $dir . '/lo-profile';
        mkdir($profile, 0775, true);
        $cmd = [
            '/usr/bin/env',
            '-u',
            'DISPLAY',
            'SAL_USE_VCLPLUGIN=svp',
            '/usr/bin/libreoffice',
            '-env:UserInstallation=' . self::fileUri($profile),
            '--headless',
            '--nologo',
            '--nodefault',
            '--nolockcheck',
            '--nofirststartwizard',
            '--convert-to',
            'pdf',
            '--outdir',
            $dir,
            $odtPath,
        ];
        $pipes = [];
        $process = proc_open($cmd, [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes);
        if (!is_resource($process)) {
            throw new RuntimeException(t('service.document.libreoffice_failed'));
        }
        $stdout = stream_get_contents($pipes[1]);
        $stderr = stream_get_contents($pipes[2]);
        fclose($pipes[1]);
        fclose($pipes[2]);
        $code = proc_close($process);
        $pdf = $dir . '/' . pathinfo($odtPath, PATHINFO_FILENAME) . '.pdf';
        if ($code !== 0 || !is_file($pdf)) {
            throw new RuntimeException(t('service.document.pdf_failed', ['details' => trim($stdout . ' ' . $stderr)]));
        }
        return $pdf;
    }

    public static function cleanup(string $path): void
    {
        $dir = dirname($path);
        if (!str_starts_with($dir, sys_get_temp_dir() . '/globinours-doc-')) {
            return;
        }
        $items = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS),
            RecursiveIteratorIterator::CHILD_FIRST,
        );
        foreach ($items as $item) {
            $item->isDir() ? rmdir($item->getPathname()) : unlink($item->getPathname());
        }
        rmdir($dir);
    }

    private static function setSpan(
        DOMXPath $xp,
        string $style,
        string $value,
        ?string $prefix = null,
        string $generatedStyle = 'GeneratedValue',
    ): void {
        $nodes = $xp->query("//text:span[@text:style-name='$style']");
        if (!$nodes || $nodes->length === 0) {
            return;
        }
        $node = $nodes->item(0);
        while ($node->firstChild) {
            $node->removeChild($node->firstChild);
        }
        $node->setAttributeNS('urn:oasis:names:tc:opendocument:xmlns:text:1.0', 'text:style-name', $generatedStyle);
        if ($prefix === 'tab') {
            $node->appendChild(
                $node->ownerDocument->createElementNS('urn:oasis:names:tc:opendocument:xmlns:text:1.0', 'text:tab'),
            );
        }
        $node->appendChild($node->ownerDocument->createTextNode($value));
    }

    private static function setLabeledSpan(
        DOMXPath $xp,
        string $style,
        string $label,
        string $value,
        ?string $prefix = null,
        string $labelStyle = 'GeneratedField',
    ): void {
        $nodes = $xp->query("//text:span[@text:style-name='$style']");
        if (!$nodes || $nodes->length === 0) {
            return;
        }
        $node = $nodes->item(0);
        while ($node->firstChild) {
            $node->removeChild($node->firstChild);
        }
        $node->setAttributeNS('urn:oasis:names:tc:opendocument:xmlns:text:1.0', 'text:style-name', $labelStyle);
        if ($prefix === 'tab') {
            $node->appendChild(
                $node->ownerDocument->createElementNS('urn:oasis:names:tc:opendocument:xmlns:text:1.0', 'text:tab'),
            );
        }
        $node->appendChild($node->ownerDocument->createTextNode($label));
        $valueSpan = $node->ownerDocument->createElementNS(
            'urn:oasis:names:tc:opendocument:xmlns:text:1.0',
            'text:span',
        );
        $valueSpan->setAttributeNS(
            'urn:oasis:names:tc:opendocument:xmlns:text:1.0',
            'text:style-name',
            'GeneratedValue',
        );
        $valueSpan->appendChild($node->ownerDocument->createTextNode($value));
        $node->appendChild($valueSpan);
    }

    private static function applySpanStyle(DOMXPath $xp, string $style, string $generatedStyle = 'GeneratedField'): void
    {
        $nodes = $xp->query("//text:span[@text:style-name='$style']");
        if (!$nodes || $nodes->length === 0) {
            return;
        }
        $nodes
            ->item(0)
            ->setAttributeNS('urn:oasis:names:tc:opendocument:xmlns:text:1.0', 'text:style-name', $generatedStyle);
    }

    private static function addGeneratedFieldStyles(DOMDocument $dom, DOMXPath $xp): void
    {
        $automatic = $xp->query('//office:automatic-styles')->item(0);
        if (!$automatic) {
            return;
        }
        self::appendTextStyle($dom, $automatic, 'GeneratedField', '11pt');
        self::appendTextStyle($dom, $automatic, 'GeneratedValue', '11pt', true);
        self::appendTextStyle($dom, $automatic, 'GeneratedSignature', '11pt');
    }

    private static function appendTextStyle(
        DOMDocument $dom,
        DOMNode $automatic,
        string $name,
        string $size,
        bool $bold = false,
    ): void {
        $style = $dom->createElementNS('urn:oasis:names:tc:opendocument:xmlns:style:1.0', 'style:style');
        $style->setAttributeNS('urn:oasis:names:tc:opendocument:xmlns:style:1.0', 'style:name', $name);
        $style->setAttributeNS('urn:oasis:names:tc:opendocument:xmlns:style:1.0', 'style:family', 'text');
        $props = $dom->createElementNS('urn:oasis:names:tc:opendocument:xmlns:style:1.0', 'style:text-properties');
        $props->setAttributeNS('urn:oasis:names:tc:opendocument:xmlns:style:1.0', 'style:font-name', 'Open Sans');
        $props->setAttributeNS('urn:oasis:names:tc:opendocument:xmlns:style:1.0', 'style:font-name-asian', 'Open Sans');
        $props->setAttributeNS(
            'urn:oasis:names:tc:opendocument:xmlns:style:1.0',
            'style:font-name-complex',
            'Open Sans',
        );
        $props->setAttributeNS('urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0', 'fo:font-size', $size);
        $weight = $bold ? 'bold' : 'normal';
        $props->setAttributeNS(
            'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0',
            'fo:font-weight',
            $weight,
        );
        $props->setAttributeNS('urn:oasis:names:tc:opendocument:xmlns:style:1.0', 'style:font-size-asian', $size);
        $props->setAttributeNS('urn:oasis:names:tc:opendocument:xmlns:style:1.0', 'style:font-size-complex', $size);
        $props->setAttributeNS('urn:oasis:names:tc:opendocument:xmlns:style:1.0', 'style:font-weight-asian', $weight);
        $props->setAttributeNS('urn:oasis:names:tc:opendocument:xmlns:style:1.0', 'style:font-weight-complex', $weight);
        $style->appendChild($props);
        $automatic->appendChild($style);
    }

    private static function transformTemplate(string $type, string $filename, callable $transform): string
    {
        $dir = self::tempDir();
        $target = $dir . '/' . $filename . '.odt';
        if (!copy(self::template($type), $target)) {
            throw new RuntimeException(t('service.document.copy_failed'));
        }
        $zip = new ZipArchive();
        if ($zip->open($target) !== true) {
            throw new RuntimeException(t('service.document.open_failed'));
        }
        $xml = $zip->getFromName('content.xml');
        if ($xml === false) {
            $zip->close();
            throw new RuntimeException(t('service.document.content_invalid'));
        }
        $dom = new DOMDocument();
        $dom->preserveWhiteSpace = true;
        if (!$dom->loadXML($xml)) {
            $zip->close();
            throw new RuntimeException(t('service.document.xml_invalid'));
        }
        $xp = new DOMXPath($dom);
        $xp->registerNamespace('text', 'urn:oasis:names:tc:opendocument:xmlns:text:1.0');
        $xp->registerNamespace('office', 'urn:oasis:names:tc:opendocument:xmlns:office:1.0');
        self::addGeneratedFieldStyles($dom, $xp);
        $transform($xp);
        $updated = $dom->saveXML();
        if ($updated === false || !$zip->addFromString('content.xml', $updated)) {
            $zip->close();
            throw new RuntimeException(t('service.document.update_failed'));
        }
        $zip->close();
        return $target;
    }

    private static function replaceParagraph(DOMXPath $xp, string $startsWith, array $parts, int $occurrence = 0): void
    {
        $matches = [];
        foreach ($xp->query('//text:p') as $paragraph) {
            $plain = preg_replace('/\s+/u', ' ', str_replace("\u{00A0}", ' ', $paragraph->textContent));
            if (str_starts_with(trim((string) $plain), $startsWith)) {
                $matches[] = $paragraph;
            }
        }
        $node = $matches[$occurrence] ?? null;
        if (!$node) {
            return;
        }
        while ($node->firstChild) {
            $node->removeChild($node->firstChild);
        }
        foreach ($parts as $part) {
            if ($part === ['tab']) {
                $node->appendChild(
                    $node->ownerDocument->createElementNS('urn:oasis:names:tc:opendocument:xmlns:text:1.0', 'text:tab'),
                );
                continue;
            }
            if ($part === ['break']) {
                $node->appendChild(
                    $node->ownerDocument->createElementNS(
                        'urn:oasis:names:tc:opendocument:xmlns:text:1.0',
                        'text:line-break',
                    ),
                );
                continue;
            }
            [$text, $kind] = $part;
            $span = $node->ownerDocument->createElementNS(
                'urn:oasis:names:tc:opendocument:xmlns:text:1.0',
                'text:span',
            );
            $span->setAttributeNS(
                'urn:oasis:names:tc:opendocument:xmlns:text:1.0',
                'text:style-name',
                $kind === 'value' ? 'GeneratedValue' : 'GeneratedField',
            );
            $span->appendChild($node->ownerDocument->createTextNode($text));
            $node->appendChild($span);
        }
    }

    private static function testLabel(string $status): string
    {
        return match ($status) {
            'neg' => 'négatif',
            'pos' => 'positif',
            default => 'non renseigné',
        };
    }
    private static function safeName(string $name): string
    {
        $name = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $name) ?: 'chat';
        return strtolower(trim(preg_replace('/[^a-zA-Z0-9]+/', '-', $name), '-')) ?: 'chat';
    }
    private static function tempDir(): string
    {
        $dir = sys_get_temp_dir() . '/globinours-doc-' . bin2hex(random_bytes(8));
        if (!mkdir($dir, 0775, true)) {
            throw new RuntimeException(t('service.document.temp_failed'));
        }
        return $dir;
    }
    private static function fileUri(string $path): string
    {
        return 'file://' . str_replace('%2F', '/', rawurlencode($path));
    }
}
