<?php

namespace EasyChat\Library\Attachment;








final class OfficeDocumentInspector {

    private const OLE_HEADER = "\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1";

    public static function definitions(): array {
        return [
            'pdf' => [
                'mime' => 'application/pdf',
                'mimes' => ['application/pdf', 'application/x-pdf'],
                'label' => 'PDF',
            ],
            'doc' => [
                'mime' => 'application/msword',
                'mimes' => ['application/msword'],
                'label' => 'DOC',
            ],
            'docx' => [
                'mime' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
                'mimes' => ['application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
                'label' => 'DOCX',
            ],
            'odt' => [
                'mime' => 'application/vnd.oasis.opendocument.text',
                'mimes' => ['application/vnd.oasis.opendocument.text'],
                'label' => 'ODT',
            ],
            'ods' => [
                'mime' => 'application/vnd.oasis.opendocument.spreadsheet',
                'mimes' => ['application/vnd.oasis.opendocument.spreadsheet'],
                'label' => 'ODS',
            ],
            'odp' => [
                'mime' => 'application/vnd.oasis.opendocument.presentation',
                'mimes' => ['application/vnd.oasis.opendocument.presentation'],
                'label' => 'ODP',
            ],
        ];
    }

    public static function extensions(): array {
        return array_keys(self::definitions());
    }

    public static function mimes(): array {
        $mimes = [];
        foreach (self::definitions() as $definition) {
            $mimes = array_merge($mimes, (array)($definition['mimes'] ?? []));
        }
        return array_values(array_unique($mimes));
    }

    public static function canonicalMime(string $extension): string {
        $definition = self::definitions()[self::normalizeExtension($extension)] ?? [];
        return (string)($definition['mime'] ?? 'application/octet-stream');
    }

    public static function extensionForMime(string $mime): string {
        $mime = strtolower(trim($mime));
        if ($mime === '') {
            return '';
        }
        foreach (self::definitions() as $extension => $definition) {
            if (in_array($mime, (array)($definition['mimes'] ?? []), true)) {
                return $extension;
            }
        }
         
         
         
        return '';
    }

    public static function label(string $extension): string {
        $extension = self::normalizeExtension($extension);
        $definition = self::definitions()[$extension] ?? [];
        return (string)($definition['label'] ?? strtoupper($extension));
    }

    public static function inspect(string $path, string $extension): array {
        $extension = self::normalizeExtension($extension);
        if (!is_file($path) || !isset(self::definitions()[$extension])) {
            return ['success' => false, 'error' => 'file_mime_denied'];
        }

        $valid = false;
        if ($extension === 'pdf') {
            $valid = self::hasHeader($path, '%PDF-');
        } elseif ($extension === 'doc') {
            $valid = self::isLegacyWordDocument($path);
        } elseif ($extension === 'docx') {
            $valid = self::isZipDocument($path, $extension, 'word/document.xml');
        } elseif (in_array($extension, ['odt', 'ods', 'odp'], true)) {
            $valid = self::isZipDocument($path, $extension, 'content.xml');
        }

        if (!$valid) {
            return ['success' => false, 'error' => 'file_mime_denied'];
        }

        return [
            'success' => true,
            'extension' => $extension,
            'mime' => self::canonicalMime($extension),
            'label' => self::label($extension),
        ];
    }

    public static function isPdf(string $path): bool {
        return self::hasHeader($path, '%PDF-');
    }

    private static function isLegacyWordDocument(string $path): bool {
        if (!self::hasHeader($path, self::OLE_HEADER)) {
            return false;
        }

         
         
        return self::fileContainsAll($path, ["W\x00o\x00r\x00d\x00D\x00o\x00c\x00u\x00m\x00e\x00n\x00t\x00"]);
    }

    private static function isZipDocument(string $path, string $extension, string $required_entry): bool {
        if (!self::hasHeader($path, "PK\x03\x04")) {
            return false;
        }

        if (class_exists('ZipArchive')) {
            return self::inspectZipArchive($path, $extension, $required_entry);
        }

         
         
         
         
         
        if (class_exists('PharData')) {
            return self::inspectPharArchive($path, $extension, $required_entry);
        }
        return false;
    }

    private static function inspectZipArchive(string $path, string $extension, string $required_entry): bool {
        $zip = new \ZipArchive();
        $opened = @$zip->open($path);
        if ($opened !== true) {
            return false;
        }

        try {
            if ($zip->numFiles <= 0 || $zip->numFiles > 4096) {
                return false;
            }

            $entries = [];
            for ($index = 0; $index < $zip->numFiles; $index++) {
                $name = $zip->getNameIndex($index);
                if (!is_string($name) || $name === '') {
                    continue;
                }
                $normalized = strtolower(str_replace('\\', '/', $name));
                $entries[$normalized] = true;
                if (substr($normalized, -14) === 'vbaproject.bin') {
                    return false;
                }
            }

            if (!isset($entries[strtolower($required_entry)])) {
                return false;
            }

            if ($extension === 'docx') {
                if (!isset($entries['[content_types].xml'])) {
                    return false;
                }
                $stat = $zip->statName('[Content_Types].xml');
                if (!is_array($stat) || (int)($stat['size'] ?? 0) <= 0 || (int)($stat['size'] ?? 0) > 1024 * 1024) {
                    return false;
                }
                $content_types = $zip->getFromName('[Content_Types].xml');
                if (!is_string($content_types)) {
                    return false;
                }
                $lower = strtolower($content_types);
                return strpos($lower, 'macroenabled') === false && strpos($lower, 'vbaproject') === false;
            }

            if (!isset($entries['mimetype'])) {
                return false;
            }
            $stat = $zip->statName('mimetype');
            if (!is_array($stat) || (int)($stat['size'] ?? 0) <= 0 || (int)($stat['size'] ?? 0) > 256) {
                return false;
            }
            $mimetype = $zip->getFromName('mimetype');
            return is_string($mimetype) && trim($mimetype) === self::canonicalMime($extension);
        } finally {
            $zip->close();
        }
    }

    private static function inspectPharArchive(string $path, string $extension, string $required_entry): bool {
        try {
            $archive = new \PharData($path);
            $entries = [];
            $count = 0;
            $prefix_path = realpath($path);
            $prefix = 'phar://' . str_replace('\\', '/', is_string($prefix_path) ? $prefix_path : $path) . '/';

            $iterator = new \RecursiveIteratorIterator($archive, \RecursiveIteratorIterator::LEAVES_ONLY);
            foreach ($iterator as $key => $file) {
                $count++;
                if ($count > 4096) {
                    return false;
                }
                $normalized_key = str_replace('\\', '/', (string)$key);
                $name = strpos($normalized_key, $prefix) === 0
                    ? substr($normalized_key, strlen($prefix))
                    : (string)$file->getFilename();
                $name = strtolower(ltrim($name, '/'));
                $entries[$name] = true;
                if (substr($name, -14) === 'vbaproject.bin') {
                    return false;
                }
            }

            if (!isset($entries[strtolower($required_entry)])) {
                return false;
            }

            if ($extension === 'docx') {
                if (!isset($entries['[content_types].xml'])) {
                    return false;
                }
                $entry = $archive['[Content_Types].xml'] ?? null;
                if (!$entry instanceof \PharFileInfo || $entry->getSize() <= 0 || $entry->getSize() > 1024 * 1024) {
                    return false;
                }
                $content_types = $entry->getContent();
                $lower = strtolower($content_types);
                return strpos($lower, 'macroenabled') === false && strpos($lower, 'vbaproject') === false;
            }

            if (!isset($entries['mimetype'])) {
                return false;
            }
            $entry = $archive['mimetype'] ?? null;
            if (!$entry instanceof \PharFileInfo || $entry->getSize() <= 0 || $entry->getSize() > 256) {
                return false;
            }
            return trim($entry->getContent()) === self::canonicalMime($extension);
        } catch (\Throwable $e) {
            return false;
        }
    }

    private static function hasHeader(string $path, string $expected): bool {
        $handle = @fopen($path, 'rb');
        if (!$handle) {
            return false;
        }
        $header = fread($handle, strlen($expected));
        fclose($handle);
        return $header === $expected;
    }

    private static function fileContainsAll(string $path, array $markers): bool {
        $remaining = [];
        foreach ($markers as $marker) {
            if ($marker !== '') {
                $remaining[(string)$marker] = true;
            }
        }
        if (!$remaining) {
            return true;
        }

        $handle = @fopen($path, 'rb');
        if (!$handle) {
            return false;
        }
        $tail = '';
        $overlap = max(array_map('strlen', array_keys($remaining))) - 1;
        while (!feof($handle) && $remaining) {
            $chunk = fread($handle, 65536);
            if ($chunk === false) {
                fclose($handle);
                return false;
            }
            $buffer = $tail . $chunk;
            foreach (array_keys($remaining) as $marker) {
                if (strpos($buffer, $marker) !== false) {
                    unset($remaining[$marker]);
                }
            }
            $tail = $overlap > 0 ? substr($buffer, -$overlap) : '';
        }
        fclose($handle);
        return !$remaining;
    }

    private static function normalizeExtension(string $extension): string {
        return strtolower(ltrim(trim($extension), '.'));
    }
}
