<?php

declare(strict_types=1);

/**
 * Exact OOXML acceptance inspector shared by preflight, static QA and live QA.
 * It deliberately avoids XPath so namespace prefixes cannot invalidate queries.
 */
final class DiarR24OoxmlInspector
{
    private const OFFICE_REL_NS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships';

    public static function loadXml(string $xml, string $errorCode): DOMDocument
    {
        if ($xml === '') {
            throw new RuntimeException($errorCode . '_EMPTY');
        }
        $doc = new DOMDocument();
        $previous = libxml_use_internal_errors(true);
        try {
            if (!$doc->loadXML($xml, LIBXML_NONET | LIBXML_COMPACT)) {
                throw new RuntimeException($errorCode . '_INVALID');
            }
        } finally {
            libxml_clear_errors();
            libxml_use_internal_errors($previous);
        }
        return $doc;
    }

    /** @return array<int,DOMElement> */
    public static function elementsByLocalName(DOMNode $root, string $localName): array
    {
        $elements = [];
        if ($root instanceof DOMDocument || $root instanceof DOMElement) {
            foreach ($root->getElementsByTagName('*') as $node) {
                if ($node instanceof DOMElement && $node->localName === $localName) {
                    $elements[] = $node;
                }
            }
        }
        return $elements;
    }

    public static function populatedWorksheetRows(string $xml, int $firstDataRow = 5): int
    {
        $doc = self::loadXml($xml, 'OOXML_WORKSHEET_XML');
        $sheetDataNodes = self::elementsByLocalName($doc, 'sheetData');
        if ($sheetDataNodes === []) {
            throw new RuntimeException('OOXML_SHEET_DATA_MISSING');
        }
        $count = 0;
        foreach ($sheetDataNodes[0]->childNodes as $row) {
            if (!$row instanceof DOMElement || $row->localName !== 'row') {
                continue;
            }
            $rowNumber = (int)$row->getAttribute('r');
            if ($rowNumber < $firstDataRow) {
                continue;
            }
            $populated = false;
            foreach ($row->childNodes as $cell) {
                if (!$cell instanceof DOMElement || $cell->localName !== 'c') {
                    continue;
                }
                foreach ($cell->getElementsByTagName('*') as $valueNode) {
                    if (!$valueNode instanceof DOMElement) {
                        continue;
                    }
                    if (in_array($valueNode->localName, ['v', 't'], true)
                        && trim((string)$valueNode->textContent) !== '') {
                        $populated = true;
                        break 2;
                    }
                }
            }
            if ($populated) {
                $count++;
            }
        }
        return $count;
    }

    public static function numericCellEquals(string $xml, string $cellReference, int $expected): bool
    {
        $doc = self::loadXml($xml, 'OOXML_WORKSHEET_XML');
        foreach (self::elementsByLocalName($doc, 'c') as $cell) {
            if (strcasecmp($cell->getAttribute('r'), $cellReference) !== 0) {
                continue;
            }
            foreach ($cell->getElementsByTagName('*') as $valueNode) {
                if ($valueNode instanceof DOMElement && $valueNode->localName === 'v') {
                    return (int)round((float)trim((string)$valueNode->textContent)) === $expected;
                }
            }
            return false;
        }
        return false;
    }

    /** @param array<string,string> $entries @return array{names:array<int,string>,paths:array<string,string>} */
    public static function workbookSheetMap(array $entries): array
    {
        if (!isset($entries['xl/workbook.xml'], $entries['xl/_rels/workbook.xml.rels'])) {
            throw new RuntimeException('OOXML_WORKBOOK_PARTS_MISSING');
        }
        $relsDoc = self::loadXml((string)$entries['xl/_rels/workbook.xml.rels'], 'OOXML_WORKBOOK_RELS');
        $targets = [];
        foreach (self::elementsByLocalName($relsDoc, 'Relationship') as $rel) {
            $id = (string)$rel->getAttribute('Id');
            $target = (string)$rel->getAttribute('Target');
            if ($id !== '' && $target !== '') {
                $targets[$id] = self::normalizeWorkbookTarget($target);
            }
        }

        $workbookDoc = self::loadXml((string)$entries['xl/workbook.xml'], 'OOXML_WORKBOOK_XML');
        $names = [];
        $paths = [];
        foreach (self::elementsByLocalName($workbookDoc, 'sheet') as $sheet) {
            $name = (string)$sheet->getAttribute('name');
            $relId = (string)$sheet->getAttributeNS(self::OFFICE_REL_NS, 'id');
            if ($relId === '') {
                foreach ($sheet->attributes as $attribute) {
                    if ($attribute !== null && $attribute->localName === 'id') {
                        $relId = (string)$attribute->nodeValue;
                        break;
                    }
                }
            }
            if ($name === '' || $relId === '' || !isset($targets[$relId])) {
                throw new RuntimeException('OOXML_WORKBOOK_SHEET_RELATIONSHIP_INVALID|' . $name);
            }
            $path = $targets[$relId];
            if (!isset($entries[$path])) {
                throw new RuntimeException('OOXML_WORKSHEET_PART_MISSING|' . $name . '|' . $path);
            }
            $names[] = $name;
            $paths[$name] = $path;
        }
        return ['names' => $names, 'paths' => $paths];
    }

    /** @param array<string,string> $entries */
    public static function worksheetXmlByName(array $entries, string $sheetName): string
    {
        $map = self::workbookSheetMap($entries);
        if (!isset($map['paths'][$sheetName])) {
            throw new RuntimeException('OOXML_WORKSHEET_NAME_NOT_FOUND|' . $sheetName);
        }
        return (string)$entries[$map['paths'][$sheetName]];
    }

    /** @param array<string,string> $entries */
    public static function worksheetDataRows(array $entries, string $sheetName, int $firstDataRow = 5): int
    {
        return self::populatedWorksheetRows(self::worksheetXmlByName($entries, $sheetName), $firstDataRow);
    }

    /** @param array<string,string> $entries */
    public static function worksheetNumericCellEquals(array $entries, string $sheetName, string $cellReference, int $expected): bool
    {
        return self::numericCellEquals(self::worksheetXmlByName($entries, $sheetName), $cellReference, $expected);
    }

    /** @param array<string,string> $entries */
    public static function worksheetDimension(array $entries, string $sheetName): string
    {
        $doc = self::loadXml(self::worksheetXmlByName($entries, $sheetName), 'OOXML_WORKSHEET_XML');
        $nodes = self::elementsByLocalName($doc, 'dimension');
        return $nodes === [] ? '' : (string)$nodes[0]->getAttribute('ref');
    }

    /** @param array<string,string> $entries */
    public static function worksheetAutoFilter(array $entries, string $sheetName): string
    {
        $doc = self::loadXml(self::worksheetXmlByName($entries, $sheetName), 'OOXML_WORKSHEET_XML');
        $nodes = self::elementsByLocalName($doc, 'autoFilter');
        return $nodes === [] ? '' : (string)$nodes[0]->getAttribute('ref');
    }

    /** @param array<string,string> $entries */
    public static function chartCount(array $entries): int
    {
        $count = 0;
        foreach (array_keys($entries) as $name) {
            if (preg_match('#^xl/charts/chart[0-9]+\.xml$#', $name) === 1) {
                $count++;
            }
        }
        return $count;
    }

    /** @param array<string,string> $entries @return array<int,string> */
    public static function slideNames(array $entries): array
    {
        $names = [];
        foreach (array_keys($entries) as $name) {
            if (preg_match('#^ppt/slides/slide[0-9]+\.xml$#', $name) === 1) {
                $names[] = $name;
            }
        }
        natsort($names);
        return array_values($names);
    }

    /** @param array<string,string> $entries @return array<int,string> */
    public static function unresolvedParts(array $entries): array
    {
        $parts = [];
        foreach ($entries as $name => $content) {
            if (substr($name, -4) !== '.xml') {
                continue;
            }
            $text = (string)$content;
            if (strpos($text, '[[') !== false || strpos($text, '#REF!') !== false) {
                $parts[] = $name;
            }
        }
        return $parts;
    }

    /** @param array<string,string> $entries @return array<int,string> */
    public static function unresolvedSlideParts(array $entries): array
    {
        $parts = [];
        foreach (self::slideNames($entries) as $name) {
            $text = (string)$entries[$name];
            if (strpos($text, '[[') !== false || strpos($text, '#REF!') !== false) {
                $parts[] = $name;
            }
        }
        return $parts;
    }

    /** @param array<string,string> $entries */
    public static function presentationIs16By9(array $entries): bool
    {
        if (!isset($entries['ppt/presentation.xml'])) {
            return false;
        }
        $doc = self::loadXml((string)$entries['ppt/presentation.xml'], 'OOXML_PRESENTATION_XML');
        $sizes = self::elementsByLocalName($doc, 'sldSz');
        if ($sizes === []) {
            return false;
        }
        $cx = (float)$sizes[0]->getAttribute('cx');
        $cy = (float)$sizes[0]->getAttribute('cy');
        if ($cx <= 0 || $cy <= 0) {
            return false;
        }
        return abs(($cx / $cy) - (16 / 9)) < 0.02;
    }

    /** @param array<string,string> $entries */
    public static function allSlidesContainText(array $entries): bool
    {
        $slides = self::slideNames($entries);
        if ($slides === []) {
            return false;
        }
        foreach ($slides as $name) {
            $doc = self::loadXml((string)$entries[$name], 'OOXML_SLIDE_XML');
            $hasText = false;
            foreach (self::elementsByLocalName($doc, 't') as $textNode) {
                if (trim((string)$textNode->textContent) !== '') {
                    $hasText = true;
                    break;
                }
            }
            if (!$hasText) {
                return false;
            }
        }
        return true;
    }

    /** @return array<string,bool> */
    public static function selfTest(): array
    {
        $checks = [];
        $default = '<?xml version="1.0"?><worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><sheetData><row r="4"><c r="A4" t="inlineStr"><is><t>H</t></is></c></row><row r="5"><c r="A5"><v>1</v></c></row><row r="6"><c r="A6" t="inlineStr"><is><t>X</t></is></c></row><row r="7"><c r="A7"/></row></sheetData></worksheet>';
        $prefixed = '<?xml version="1.0"?><x:worksheet xmlns:x="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><x:sheetData><x:row r="5"><x:c r="A5"><x:v>1</x:v></x:c></x:row><x:row r="6"><x:c r="A6" t="inlineStr"><x:is><x:t>X</x:t></x:is></x:c></x:row></x:sheetData></x:worksheet>';
        $plain = '<?xml version="1.0"?><worksheet><sheetData><row r="5"><c r="A5"><v>1</v></c></row></sheetData></worksheet>';
        $checks['default_namespace_rows'] = self::populatedWorksheetRows($default, 5) === 2;
        $checks['prefixed_namespace_rows'] = self::populatedWorksheetRows($prefixed, 5) === 2;
        $checks['unprefixed_rows'] = self::populatedWorksheetRows($plain, 5) === 1;
        $checks['empty_row_not_counted'] = self::populatedWorksheetRows($default, 7) === 0;
        $checks['numeric_cell_default'] = self::numericCellEquals($default, 'A5', 1);
        $checks['numeric_cell_prefixed'] = self::numericCellEquals($prefixed, 'A5', 1);

        $entries = [
            'xl/workbook.xml' => '<?xml version="1.0"?><workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><sheets><sheet name="Summary" sheetId="1" r:id="rId1"/><sheet name="All Cases" sheetId="2" r:id="rId2"/></sheets></workbook>',
            'xl/_rels/workbook.xml.rels' => '<?xml version="1.0"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Target="worksheets/sheet1.xml"/><Relationship Id="rId2" Target="worksheets/sheet2.xml"/></Relationships>',
            'xl/worksheets/sheet1.xml' => $default,
            'xl/worksheets/sheet2.xml' => $prefixed,
        ];
        $map = self::workbookSheetMap($entries);
        $checks['workbook_sheet_order'] = $map['names'] === ['Summary', 'All Cases'];
        $checks['workbook_sheet_path'] = isset($map['paths']['All Cases']) && $map['paths']['All Cases'] === 'xl/worksheets/sheet2.xml';
        $checks['sheet_name_row_count'] = self::worksheetDataRows($entries, 'All Cases', 5) === 2;
        return $checks;
    }

    private static function normalizeWorkbookTarget(string $target): string
    {
        $target = str_replace('\\', '/', trim($target));
        while (strpos($target, '../') === 0) {
            $target = substr($target, 3);
        }
        $target = ltrim($target, '/');
        if (strpos($target, 'xl/') === 0) {
            return $target;
        }
        return 'xl/' . $target;
    }
}
