<?php

declare(strict_types=1);

namespace App\Services;

use PharData;
use Throwable;
use ZipArchive;

/**
 * Fail-closed trust gate for server-side PBIX availability.
 *
 * Important: structural validation is deliberately not described as proof that
 * Power BI Desktop can open or refresh a report. Final availability additionally
 * requires a hash-bound Desktop acceptance receipt for the exact PBIX bytes.
 */
final class PowerBiReportValidator
{
    public const GATE_VERSION = 'DDB_POWERBI_PBIX_TRUST_GATE_V1';
    public const ACCEPTANCE_SCHEMA = 'DDB_POWERBI_DESKTOP_ACCEPTANCE_V1';
    public const MASTER_RELATIVE_PATH = 'resources/powerbi/Daily_Inpatient_Revenue_Master.pbix';
    public const LEGACY_RELATIVE_PATH = 'resources/powerbi/NJCH_Daily_Revenue.pbix';
    public const PACKAGE_NAME = 'Daily_Inpatient_Revenue_Master.pbix';

    /** @var string */
    private $projectRoot;

    public function __construct(?string $projectRoot = null)
    {
        $root = $projectRoot !== null ? $projectRoot : base_path();
        $this->projectRoot = rtrim($root, DIRECTORY_SEPARATOR . '/\\');
    }

    /**
     * @return array<string,mixed>
     */
    public function status(): array
    {
        $candidates = [self::MASTER_RELATIVE_PATH, self::LEGACY_RELATIVE_PATH];
        $results = [];

        foreach ($candidates as $relative) {
            $path = $this->absolutePath($relative);
            $result = $this->validatePath($path, $relative);
            $results[] = $result;
            if (($result['available'] ?? false) === true) {
                $result['candidates'] = $this->publicCandidateResults($results);
                return $result;
            }
        }

        $state = 'MISSING';
        $reason = 'PBIX_FILE_NOT_FOUND';
        foreach ($results as $result) {
            $candidateState = (string)($result['state'] ?? 'INVALID');
            if ($candidateState === 'STRUCTURALLY_VALID_PENDING_DESKTOP_ACCEPTANCE') {
                $state = $candidateState;
                $reason = (string)($result['reason'] ?? 'DESKTOP_ACCEPTANCE_REQUIRED');
                break;
            }
            if ($candidateState === 'INVALID') {
                $state = 'INVALID';
                $reason = (string)($result['reason'] ?? 'PBIX_VALIDATION_FAILED');
            }
        }

        return [
            'gate_version' => self::GATE_VERSION,
            'available' => false,
            'state' => $state,
            'reason' => $reason,
            'selected_relative_path' => null,
            'package_name' => self::PACKAGE_NAME,
            'desktop_acceptance_required' => true,
            'candidates' => $this->publicCandidateResults($results),
        ];
    }

    /**
     * @return array<string,mixed>
     */
    /** @param array<string,mixed> $status */
    public function acceptedReportPath(array $status): ?string
    {
        if (($status['available'] ?? false) !== true) {
            return null;
        }
        $relative = (string)($status['selected_relative_path'] ?? '');
        return $relative !== '' ? $this->absolutePath($relative) : null;
    }

    /**
     * @return array<string,mixed>
     */
    public function validatePath(string $path, string $relativePath = ''): array
    {
        $relative = $relativePath !== '' ? $relativePath : basename($path);
        $checks = [];
        $add = static function (string $name, bool $pass, string $detail = '') use (&$checks): void {
            $checks[] = [
                'name' => $name,
                'status' => $pass ? 'PASS' : 'FAIL',
                'detail' => $detail,
            ];
        };

        $exists = is_file($path);
        $add('file_exists', $exists, $exists ? $relative : 'missing');
        if (!$exists) {
            return $this->result(false, 'MISSING', 'PBIX_FILE_NOT_FOUND', $relative, 0, '', $checks);
        }

        $readable = is_readable($path);
        $add('file_readable', $readable, $readable ? 'readable' : 'not_readable');

        $extensionOk = strtolower((string)pathinfo($path, PATHINFO_EXTENSION)) === 'pbix';
        $add('pbix_extension', $extensionOk, (string)pathinfo($path, PATHINFO_EXTENSION));

        $size = @filesize($path);
        $sizeValue = $size === false ? 0 : (int)$size;
        $sizeOk = $sizeValue >= 1024;
        $add('minimum_size_1kb', $sizeOk, (string)$sizeValue);

        $signature = '';
        if ($readable) {
            $handle = @fopen($path, 'rb');
            if ($handle !== false) {
                $signature = (string)fread($handle, 4);
                fclose($handle);
            }
        }
        $signatureOk = substr($signature, 0, 2) === 'PK';
        $add('zip_container_signature', $signatureOk, bin2hex($signature));

        if (!$readable || !$extensionOk || !$sizeOk || !$signatureOk) {
            return $this->result(false, 'INVALID', 'PBIX_BASIC_FILE_GATE_FAILED', $relative, $sizeValue, '', $checks);
        }

        $entries = $this->archiveEntries($path);
        $archiveReadable = $entries !== null;
        $add('archive_readable', $archiveReadable, $archiveReadable ? 'entries=' . count($entries) : 'archive_open_failed');
        if (!$archiveReadable) {
            return $this->result(false, 'INVALID', 'PBIX_ARCHIVE_UNREADABLE', $relative, $sizeValue, '', $checks);
        }

        $normalized = [];
        $safePaths = true;
        foreach ($entries as $entry) {
            $name = str_replace('\\', '/', trim((string)$entry));
            if ($name === '') {
                continue;
            }
            if ($name[0] === '/' || strpos($name, '../') !== false || strpos($name, ':') !== false) {
                $safePaths = false;
            }
            $normalized[] = trim($name, '/');
        }
        $entrySet = array_fill_keys($normalized, true);

        $add('archive_has_multiple_parts', count($normalized) >= 3, 'entries=' . count($normalized));
        $add('archive_paths_safe', $safePaths, $safePaths ? 'safe' : 'unsafe_path_detected');

        $hasVersion = isset($entrySet['Version']);
        $add('powerbi_version_part', $hasVersion, $hasVersion ? 'Version' : 'missing');

        $reportMarkers = ['Report/Layout', 'Report/definition.pbir', 'Report/Sections'];
        $reportMarker = $this->firstPresent($entrySet, $reportMarkers);
        $hasReport = $reportMarker !== null;
        $add('powerbi_report_definition_part', $hasReport, $reportMarker !== null ? $reportMarker : 'missing');

        $modelMarkers = ['DataModel', 'DataMashup', 'Connections', 'Metadata', 'DiagramState'];
        $modelMarker = $this->firstPresent($entrySet, $modelMarkers);
        $hasModelOrConnection = $modelMarker !== null;
        $add('powerbi_model_or_connection_part', $hasModelOrConnection, $modelMarker !== null ? $modelMarker : 'missing');

        $structureOk = count($normalized) >= 3 && $safePaths && $hasVersion && $hasReport && $hasModelOrConnection;
        $add('pbix_structural_heuristic', $structureOk, $structureOk ? 'admissible_for_desktop_acceptance' : 'required_powerbi_parts_missing');

        $sha256 = $structureOk ? (string)@hash_file('sha256', $path) : '';
        if (!$structureOk || $sha256 === '') {
            return $this->result(false, 'INVALID', 'PBIX_POWERBI_STRUCTURE_GATE_FAILED', $relative, $sizeValue, $sha256, $checks);
        }

        $acceptance = $this->validateAcceptanceReceipt($path, $sha256);
        foreach ((array)($acceptance['checks'] ?? []) as $check) {
            if (is_array($check)) {
                $checks[] = $check;
            }
        }

        if (($acceptance['accepted'] ?? false) !== true) {
            return $this->result(
                false,
                'STRUCTURALLY_VALID_PENDING_DESKTOP_ACCEPTANCE',
                (string)($acceptance['reason'] ?? 'DESKTOP_ACCEPTANCE_REQUIRED'),
                $relative,
                $sizeValue,
                $sha256,
                $checks,
                (string)($acceptance['receipt_relative_path'] ?? '')
            );
        }

        return $this->result(
            true,
            'ACCEPTED',
            'PBIX_TRUST_GATE_PASSED',
            $relative,
            $sizeValue,
            $sha256,
            $checks,
            (string)($acceptance['receipt_relative_path'] ?? '')
        );
    }

    /**
     * @return array<string,mixed>
     */
    private function validateAcceptanceReceipt(string $pbixPath, string $sha256): array
    {
        $receiptPath = $pbixPath . '.acceptance.json';
        $relativeReceipt = $this->relativePath($receiptPath);
        $checks = [];
        $add = static function (string $name, bool $pass, string $detail = '') use (&$checks): void {
            $checks[] = ['name' => $name, 'status' => $pass ? 'PASS' : 'FAIL', 'detail' => $detail];
        };

        $exists = is_file($receiptPath);
        $add('desktop_acceptance_receipt_exists', $exists, $exists ? $relativeReceipt : 'missing');
        if (!$exists) {
            return [
                'accepted' => false,
                'reason' => 'DESKTOP_ACCEPTANCE_RECEIPT_MISSING',
                'receipt_relative_path' => $relativeReceipt,
                'checks' => $checks,
            ];
        }

        $raw = @file_get_contents($receiptPath);
        $data = is_string($raw) ? json_decode($raw, true) : null;
        $jsonOk = is_array($data);
        $add('desktop_acceptance_receipt_json', $jsonOk, $jsonOk ? 'json_object' : 'invalid_json');
        if (!$jsonOk) {
            return [
                'accepted' => false,
                'reason' => 'DESKTOP_ACCEPTANCE_RECEIPT_INVALID_JSON',
                'receipt_relative_path' => $relativeReceipt,
                'checks' => $checks,
            ];
        }

        $schemaOk = (string)($data['schema'] ?? '') === self::ACCEPTANCE_SCHEMA;
        $add('desktop_acceptance_schema', $schemaOk, (string)($data['schema'] ?? ''));

        $receiptHash = strtolower(trim((string)($data['pbix_sha256'] ?? '')));
        $hashOk = $receiptHash !== '' && hash_equals(strtolower($sha256), $receiptHash);
        $add('desktop_acceptance_hash_binding', $hashOk, $hashOk ? 'sha256_match' : 'sha256_mismatch');

        $required = [
            'power_bi_desktop_open',
            'refresh_from_datapack',
            'njch_reconciliation',
            'mnh_reconciliation',
        ];
        $allPass = true;
        foreach ($required as $key) {
            $pass = strtoupper(trim((string)($data[$key] ?? ''))) === 'PASS';
            $add('desktop_acceptance_' . $key, $pass, (string)($data[$key] ?? 'missing'));
            $allPass = $allPass && $pass;
        }

        $acceptedAt = trim((string)($data['accepted_at'] ?? ''));
        $acceptedAtOk = $acceptedAt !== '' && strtotime($acceptedAt) !== false;
        $add('desktop_acceptance_timestamp', $acceptedAtOk, $acceptedAt !== '' ? $acceptedAt : 'missing');

        $acceptedBy = trim((string)($data['accepted_by'] ?? ''));
        $acceptedByOk = $acceptedBy !== '' && stripos($acceptedBy, 'REPLACE_WITH') === false;
        $add('desktop_acceptance_actor', $acceptedByOk, $acceptedBy !== '' ? $acceptedBy : 'missing');

        $evidenceReference = trim((string)($data['evidence_reference'] ?? ''));
        $evidenceReferenceOk = $evidenceReference !== '' && stripos($evidenceReference, 'REPLACE_WITH') === false;
        $add('desktop_acceptance_evidence_reference', $evidenceReferenceOk, $evidenceReference !== '' ? $evidenceReference : 'missing');

        $accepted = $schemaOk && $hashOk && $allPass && $acceptedAtOk && $acceptedByOk && $evidenceReferenceOk;
        return [
            'accepted' => $accepted,
            'reason' => $accepted ? 'PBIX_DESKTOP_ACCEPTANCE_RECEIPT_VALID' : 'DESKTOP_ACCEPTANCE_RECEIPT_GATE_FAILED',
            'receipt_relative_path' => $relativeReceipt,
            'checks' => $checks,
        ];
    }

    /** @return list<string>|null */
    private function archiveEntries(string $path): ?array
    {
        if (class_exists(ZipArchive::class)) {
            $zip = new ZipArchive();
            $opened = $zip->open($path);
            if ($opened !== true) {
                return null;
            }
            $entries = [];
            for ($i = 0; $i < $zip->numFiles; $i++) {
                $name = $zip->getNameIndex($i);
                if (is_string($name)) {
                    $entries[] = $name;
                }
            }
            $zip->close();
            return $entries;
        }

        if (class_exists(PharData::class)) {
            try {
                $archive = new PharData($path);
                $entries = [];
                $uri = 'phar://' . str_replace('\\', '/', $path);
                $prefix = rtrim($uri, '/') . '/';
                $directory = new \RecursiveDirectoryIterator($uri, \FilesystemIterator::SKIP_DOTS);
                $iterator = new \RecursiveIteratorIterator($directory, \RecursiveIteratorIterator::SELF_FIRST);
                foreach ($iterator as $key => $file) {
                    $name = str_replace('\\', '/', (string)$key);
                    if (strpos($name, $prefix) === 0) {
                        $name = substr($name, strlen($prefix));
                    }
                    if ($name !== '') {
                        $entries[] = trim($name, '/');
                    }
                }
                unset($archive);
                return array_values(array_unique($entries));
            } catch (Throwable $e) {
                return null;
            }
        }

        return null;
    }

    /**
     * @param array<string,bool> $entrySet
     * @param list<string> $markers
     */
    private function firstPresent(array $entrySet, array $markers): ?string
    {
        foreach ($markers as $marker) {
            if (isset($entrySet[$marker])) {
                return $marker;
            }
        }
        return null;
    }

    /**
     * @param list<array<string,mixed>> $checks
     * @return array<string,mixed>
     */
    private function result(bool $available, string $state, string $reason, string $relative, int $size, string $sha256, array $checks, string $receiptRelative = ''): array
    {
        return [
            'gate_version' => self::GATE_VERSION,
            'available' => $available,
            'state' => $state,
            'reason' => $reason,
            'selected_relative_path' => $available ? $relative : null,
            'candidate_relative_path' => $relative,
            'package_name' => self::PACKAGE_NAME,
            'size_bytes' => $size,
            'sha256' => $sha256,
            'desktop_acceptance_required' => true,
            'acceptance_receipt_relative_path' => $receiptRelative,
            'checks' => $checks,
        ];
    }

    /** @param list<array<string,mixed>> $results @return list<array<string,mixed>> */
    private function publicCandidateResults(array $results): array
    {
        $public = [];
        foreach ($results as $result) {
            $public[] = [
                'candidate_relative_path' => (string)($result['candidate_relative_path'] ?? ''),
                'available' => (bool)($result['available'] ?? false),
                'state' => (string)($result['state'] ?? ''),
                'reason' => (string)($result['reason'] ?? ''),
                'size_bytes' => (int)($result['size_bytes'] ?? 0),
                'sha256' => (string)($result['sha256'] ?? ''),
                'acceptance_receipt_relative_path' => (string)($result['acceptance_receipt_relative_path'] ?? ''),
            ];
        }
        return $public;
    }

    private function absolutePath(string $relative): string
    {
        return $this->projectRoot . DIRECTORY_SEPARATOR . str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $relative);
    }

    private function relativePath(string $path): string
    {
        $root = $this->projectRoot . DIRECTORY_SEPARATOR;
        if (strpos($path, $root) === 0) {
            return str_replace('\\', '/', substr($path, strlen($root)));
        }
        return basename($path);
    }
}
