<?php

/**
 * LolaCore - PlaybookDocumentReader
 *
 * Extracts plain text from uploaded playbook source documents (TXT/MD/DOCX/HTML).
 *
 * @package LolaCore
 * @license GPL-2.0-or-later
 */

declare(strict_types=1);

namespace LolaCore\Playbooks;

// phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Reader internals: upload/size diagnostic messages; escaped at the presentation layer.

use LolaCore\Playbooks\Exceptions\PlaybookDocumentTooLongException;

class PlaybookDocumentReader
{
    public const MAX_FILE_BYTES       = 5_242_880; // 5 MB
    public const MAX_EXTRACTED_CHARS  = 40_000;

    /** @var array<int,string> */
    private const ALLOWED_EXTENSIONS = ['txt', 'md', 'markdown', 'docx', 'html', 'htm'];

    /**
     * @param array<string,mixed> $file PHP $_FILES-style array.
     * @throws \InvalidArgumentException
     */
    public function readUploadedFile(array $file): string
    {
        $error = (int) ($file['error'] ?? UPLOAD_ERR_NO_FILE);
        if ($error !== UPLOAD_ERR_OK) {
            throw new \InvalidArgumentException($this->uploadErrorMessage($error));
        }

        $tmpName = (string) ($file['tmp_name'] ?? '');
        if ($tmpName === '' || !is_uploaded_file($tmpName)) {
            throw new \InvalidArgumentException('Invalid upload payload.');
        }

        $size = (int) ($file['size'] ?? 0);
        if ($size > self::MAX_FILE_BYTES) {
            throw new \InvalidArgumentException(
                sprintf(
                    'File exceeds the %s upload limit. Split the document or paste a shorter excerpt.',
                    size_format(self::MAX_FILE_BYTES)
                )
            );
        }

        $name = (string) ($file['name'] ?? 'document');
        $ext  = strtolower(pathinfo(sanitize_file_name($name), PATHINFO_EXTENSION));
        $this->assertAllowedExtension($ext);

        $raw = $this->readFromPath($tmpName, $ext);
        return $this->enforceExtractedLimit($raw);
    }

    /**
     * @throws \InvalidArgumentException
     */
    public function readFromPath(string $path, string $extension): string
    {
        $ext = strtolower(ltrim($extension, '.'));
        $this->assertAllowedExtension($ext);

        if (!is_readable($path)) {
            throw new \InvalidArgumentException('Document file is not readable.');
        }

        $raw = match ($ext) {
            'txt', 'md', 'markdown' => $this->readPlainTextFile($path),
            'html', 'htm'           => $this->readHtmlFile($path),
            'docx'                  => $this->readDocxFile($path),
            default                 => throw new \InvalidArgumentException('Unsupported document type.'),
        };

        return $this->normalize($raw);
    }

    public function normalize(string $text): string
    {
        $text = preg_replace('/^\xEF\xBB\xBF/', '', $text) ?? $text;
        $text = str_replace(["\r\n", "\r"], "\n", $text);
        $text = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $text) ?? $text;
        $text = preg_replace("/\n{3,}/", "\n\n", $text) ?? $text;

        return trim($text);
    }

    /**
     * @throws PlaybookDocumentTooLongException
     */
    public function enforceExtractedLimit(string $text): string
    {
        $charCount = mb_strlen($text, 'UTF-8');
        if ($charCount > self::MAX_EXTRACTED_CHARS) {
            throw new PlaybookDocumentTooLongException($charCount, self::MAX_EXTRACTED_CHARS);
        }

        return $text;
    }

    private function readPlainTextFile(string $path): string
    {
        $content = @file_get_contents($path);
        if ($content === false) {
            throw new \InvalidArgumentException('Failed to read the text file.');
        }

        return $content;
    }

    private function readHtmlFile(string $path): string
    {
        $content = @file_get_contents($path);
        if ($content === false) {
            throw new \InvalidArgumentException('Failed to read the HTML file.');
        }

        $text = wp_strip_all_tags($content, true);
        return html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
    }

    private function readDocxFile(string $path): string
    {
        if (!class_exists('ZipArchive')) {
            throw new \InvalidArgumentException(
                'DOCX support requires the PHP Zip extension. Ask your host to enable php_zip, or upload a .md or .txt export instead.'
            );
        }

        $zip = new \ZipArchive();
        $opened = $zip->open($path);
        if ($opened !== true) {
            throw new \InvalidArgumentException('Failed to open DOCX archive. The file may be corrupt.');
        }

        $xml = $zip->getFromName('word/document.xml');
        $zip->close();

        if ($xml === false || $xml === '') {
            throw new \InvalidArgumentException('DOCX file is missing word/document.xml.');
        }

        return $this->extractDocxText($xml);
    }

    private function extractDocxText(string $xml): string
    {
        $previous = libxml_use_internal_errors(true);
        $dom      = new \DOMDocument();
        $loaded   = $dom->loadXML($xml);
        libxml_clear_errors();
        libxml_use_internal_errors($previous);

        if (!$loaded) {
            throw new \InvalidArgumentException('Failed to parse DOCX document XML.');
        }

        $xpath = new \DOMXPath($dom);
        $xpath->registerNamespace('w', 'http://schemas.openxmlformats.org/wordprocessingml/2006/main');
        $nodes = $xpath->query('//w:t');

        if ($nodes === false) {
            return '';
        }

        $parts = [];
        foreach ($nodes as $node) {
            $parts[] = $node->textContent;
        }

        $text = implode(' ', $parts);
        $text = preg_replace('/[ \t]+/', ' ', $text) ?? $text;

        return trim($text);
    }

    private function assertAllowedExtension(string $ext): void
    {
        if (!in_array($ext, self::ALLOWED_EXTENSIONS, true)) {
            throw new \InvalidArgumentException(
                'Unsupported file type. Upload TXT, MD, DOCX, or HTML.'
            );
        }
    }

    private function uploadErrorMessage(int $code): string
    {
        return match ($code) {
            UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE => 'Uploaded file exceeds the server size limit.',
            UPLOAD_ERR_PARTIAL                      => 'Upload was interrupted. Try again.',
            UPLOAD_ERR_NO_FILE                        => 'No file was uploaded.',
            default                                   => 'File upload failed.',
        };
    }
}
