<?php

declare(strict_types=1);

namespace App\Services;

use App\Repositories\DiarReportDataRepository;
use RuntimeException;

/**
 * Filesystem-only prebuilt report cache for DIAR-6B.5-R2.5.
 *
 * Oracle source access remains read-only. The service stores only generated
 * JSON/XLSX/PPTX artifacts under storage/diar-reports/prebuilt.
 */
final class DiarPrebuiltReportCacheService
{
    private const RELEASE = 'DIAR-6B.5-R2.5';
    private const STORAGE_RELATIVE = 'storage/diar-reports/prebuilt';
    private const KEEP_GENERATIONS = 3;

    /** @return array<string,mixed> */
    public function load(array $allowedSites, array $filters): array
    {
        $scope = $this->resolveScope($allowedSites, (string)($filters['site'] ?? 'NJCH'));
        $requestedMonth = $this->normaliseMonth((string)($filters['month'] ?? ''));
        $pointerPath = $this->pointerPath($scope, $requestedMonth === '' ? 'latest' : $requestedMonth);
        $pointer = $this->readJson($pointerPath);
        if ($pointer === []) {
            throw new RuntimeException('DIAR_REPORT_CACHE_MISS|' . $scope . '|' . ($requestedMonth !== '' ? $requestedMonth : 'LATEST'));
        }

        $generation = trim((string)($pointer['generation'] ?? ''));
        if ($generation === '' || preg_match('/^[A-Z0-9_\/-]+$/i', $generation) !== 1) {
            throw new RuntimeException('DIAR_REPORT_CACHE_POINTER_INVALID');
        }

        $dir = $this->root() . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $generation);
        $manifest = $this->readJson($dir . DIRECTORY_SEPARATOR . 'manifest.json');
        $page = $this->readJson($dir . DIRECTORY_SEPARATOR . 'page.json');
        if ($manifest === [] || $page === []) {
            throw new RuntimeException('DIAR_REPORT_CACHE_CONTENT_MISSING');
        }

        $manifestScope = strtoupper((string)($manifest['scope_code'] ?? ''));
        if ($manifestScope !== $scope) {
            throw new RuntimeException('DIAR_REPORT_CACHE_SCOPE_MISMATCH');
        }
        if (!$this->scopeAllowed($allowedSites, $manifestScope)) {
            throw new RuntimeException('DIAR_REPORT_CACHE_SCOPE_FORBIDDEN');
        }

        $page['cache'] = [
            'release' => self::RELEASE,
            'prepared_at' => (string)($manifest['prepared_at'] ?? ''),
            'scope_code' => $manifestScope,
            'selected_month' => (string)($manifest['selected_month'] ?? ''),
            'export_ready' => !empty($manifest['export_ready']),
            'source' => 'PREBUILT',
        ];

        return [
            'report' => $page,
            'manifest' => $manifest,
            'directory' => $dir,
            'excel_path' => $dir . DIRECTORY_SEPARATOR . 'report.xlsx',
            'powerpoint_path' => $dir . DIRECTORY_SEPARATOR . 'report.pptx',
            'cache_hit' => true,
        ];
    }

    /** @return array<string,mixed> */
    public function build(array $allowedSites, array $filters, bool $withExports = true, string $generatedBy = 'SYSTEM'): array
    {
        $scope = $this->resolveScope($allowedSites, (string)($filters['site'] ?? 'NJCH'));
        $requestedMonth = $this->normaliseMonth((string)($filters['month'] ?? ''));
        $lockKey = strtolower($scope . '_' . ($requestedMonth !== '' ? $requestedMonth : 'latest'));
        $lockPath = $this->root() . DIRECTORY_SEPARATOR . 'locks' . DIRECTORY_SEPARATOR . $lockKey . '.lock';
        $this->ensureDirectory(dirname($lockPath));

        $lock = fopen($lockPath, 'c+');
        if ($lock === false) {
            throw new RuntimeException('DIAR_REPORT_CACHE_LOCK_OPEN_FAILED');
        }

        $locked = false;
        try {
            $started = microtime(true);
            do {
                $locked = flock($lock, LOCK_EX | LOCK_NB);
                if (!$locked) {
                    usleep(200000);
                }
            } while (!$locked && (microtime(true) - $started) < 120.0);

            if (!$locked) {
                throw new RuntimeException('DIAR_REPORT_CACHE_BUILD_ALREADY_RUNNING');
            }

            $filters['site'] = $scope;
            $filters['period'] = 'latest_closed';
            $filters['month'] = $requestedMonth;
            $sites = $scope === 'ALL' ? ['MNH', 'NJCH'] : [$scope];

            $report = (new DiarReportDataRepository())->build($sites, $filters, true);
            $errors = array_values((array)($report['errors'] ?? []));
            if ($errors !== []) {
                throw new RuntimeException('DIAR_REPORT_CACHE_SOURCE_ERRORS|' . implode(';', $errors));
            }

            $selectedMonth = $this->normaliseMonth((string)($report['selected_month'] ?? ''));
            if ($selectedMonth === '') {
                throw new RuntimeException('DIAR_REPORT_CACHE_MONTH_MISSING');
            }

            $this->validateReport($report, $scope);
            $signature = $this->signature($report, $scope, $selectedMonth, $withExports);
            $generationRel = 'generations/' . $scope . '/' . $selectedMonth . '/' . $signature;
            $generationDir = $this->root() . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $generationRel);

            if (!is_dir($generationDir)) {
                $tempDir = $generationDir . '.tmp_' . substr(bin2hex(random_bytes(5)), 0, 10);
                $this->ensureDirectory($tempDir);
                try {
                    $this->writeGeneration($tempDir, $report, $scope, $selectedMonth, $withExports, $generatedBy);
                    $this->ensureDirectory(dirname($generationDir));
                    if (!@rename($tempDir, $generationDir)) {
                        if (!is_dir($generationDir)) {
                            throw new RuntimeException('DIAR_REPORT_CACHE_PROMOTE_FAILED');
                        }
                        $this->removeTree($tempDir);
                    }
                } catch (\Throwable $e) {
                    $this->removeTree($tempDir);
                    throw $e;
                }
            } elseif ($withExports) {
                $manifest = $this->readJson($generationDir . DIRECTORY_SEPARATOR . 'manifest.json');
                if (empty($manifest['export_ready'])) {
                    throw new RuntimeException('DIAR_REPORT_CACHE_GENERATION_INCOMPLETE');
                }
            }

            $pointer = [
                'release' => self::RELEASE,
                'scope_code' => $scope,
                'selected_month' => $selectedMonth,
                'generation' => $generationRel,
                'updated_at' => date('c'),
            ];
            $this->writeJsonAtomic($this->pointerPath($scope, $selectedMonth), $pointer);
            if ($requestedMonth === '') {
                $this->writeJsonAtomic($this->pointerPath($scope, 'latest'), $pointer);
            }

            $this->cleanupOldGenerations($scope, $selectedMonth, $signature);
            return $this->load($allowedSites, ['site' => $scope, 'month' => $selectedMonth]);
        } finally {
            if ($locked) {
                flock($lock, LOCK_UN);
            }
            fclose($lock);
        }
    }

    /** @return array<int,array<string,mixed>> */
    public function buildAll(array $allowedSites, string $month = '', string $generatedBy = 'SYSTEM'): array
    {
        $results = [];
        $normalisedAllowed = array_values(array_intersect(['NJCH', 'MNH'], array_map('strtoupper', $allowedSites)));
        foreach ($normalisedAllowed as $scope) {
            $results[] = $this->build($normalisedAllowed, [
                'site' => $scope,
                'period' => 'latest_closed',
                'month' => $month,
            ], true, $generatedBy);
        }
        if (in_array('NJCH', $normalisedAllowed, true) && in_array('MNH', $normalisedAllowed, true)) {
            $results[] = $this->build($normalisedAllowed, [
                'site' => 'ALL',
                'period' => 'latest_closed',
                'month' => $month,
            ], true, $generatedBy);
        }
        return $results;
    }

    /** @return array<string,mixed> */
    public function ensure(array $allowedSites, array $filters, string $generatedBy = 'SYSTEM'): array
    {
        try {
            $cached = $this->load($allowedSites, $filters);
            $manifest = (array)($cached['manifest'] ?? []);
            $excelReady = is_file((string)($cached['excel_path'] ?? ''));
            $pptReady = is_file((string)($cached['powerpoint_path'] ?? ''));
            if (!empty($manifest['export_ready']) && $excelReady && $pptReady) {
                return $cached;
            }
        } catch (\Throwable $e) {
            // Build below.
        }
        return $this->build($allowedSites, $filters, true, $generatedBy);
    }

    /** @return array<string,mixed> */
    public function prepareDownload(
        string $type,
        array $allowedSites,
        array $filters,
        string $username
    ): array {
        $type = strtoupper(trim($type));
        if (!in_array($type, ['XLSX', 'PPTX'], true)) {
            throw new RuntimeException('DIAR_REPORT_EXPORT_TYPE_INVALID');
        }

        $cached = $this->ensure($allowedSites, $filters, $username);
        $report = (array)($cached['report'] ?? []);
        $sourcePath = $type === 'XLSX'
            ? (string)($cached['excel_path'] ?? '')
            : (string)($cached['powerpoint_path'] ?? '');
        if (!is_file($sourcePath)) {
            throw new RuntimeException('DIAR_REPORT_PREBUILT_EXPORT_MISSING');
        }

        $manifestService = new DiarExportManifestService();
        $allocation = $manifestService->allocate($report, $type);
        if (!copy($sourcePath, (string)$allocation['file_path'])) {
            throw new RuntimeException('DIAR_REPORT_FAST_EXPORT_COPY_FAILED');
        }

        $cacheManifest = (array)($cached['manifest'] ?? []);
        $manifest = $manifestService->register($report, $allocation, $type, $username, [
            'delivery_release' => self::RELEASE,
            'delivery_mode' => 'PREBUILT_FAST_COPY',
            'cache_prepared_at' => (string)($cacheManifest['prepared_at'] ?? ''),
            'cache_source_sha256' => strtolower((string)hash_file('sha256', $sourcePath)),
            'sheet_count' => $type === 'XLSX' ? 12 : null,
            'slide_count' => $type === 'PPTX' ? 12 : null,
        ]);
        $manifest['release'] = self::RELEASE;
        $json = json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        if ($json === false || file_put_contents((string)$allocation['manifest_path'], $json) === false) {
            throw new RuntimeException('DIAR_REPORT_FAST_EXPORT_MANIFEST_UPDATE_FAILED');
        }

        return [
            'file_path' => (string)$allocation['file_path'],
            'filename' => (string)$allocation['filename'],
            'manifest' => $manifest,
            'cache_manifest' => $cacheManifest,
        ];
    }

    private function writeGeneration(
        string $dir,
        array $report,
        string $scope,
        string $selectedMonth,
        bool $withExports,
        string $generatedBy
    ): void {
        $pageReport = $report;
        $pageReport['movement_detail_count'] = count((array)($report['movement_details'] ?? []));
        unset($pageReport['movement_details']);
        $pageReport['cache'] = [
            'release' => self::RELEASE,
            'prepared_at' => date('c'),
            'scope_code' => $scope,
            'selected_month' => $selectedMonth,
            'export_ready' => $withExports,
            'source' => 'PREBUILT',
        ];

        $this->writeJsonAtomic($dir . DIRECTORY_SEPARATOR . 'page.json', $pageReport);
        $this->writeJsonAtomic($dir . DIRECTORY_SEPARATOR . 'report.json', $report);

        $excelPath = $dir . DIRECTORY_SEPARATOR . 'report.xlsx';
        $pptPath = $dir . DIRECTORY_SEPARATOR . 'report.pptx';
        if ($withExports) {
            (new DiarApprovedWorkbookExportService())->create($excelPath, $report);
            (new DiarPowerPointExportService())->create($pptPath, $report, $generatedBy);
        }

        $summary = (array)($report['summary'] ?? []);
        $manifest = [
            'release' => self::RELEASE,
            'scope_code' => $scope,
            'selected_month' => $selectedMonth,
            'month_status' => (string)($report['month_status'] ?? ''),
            'prepared_at' => date('c'),
            'prepared_by' => $generatedBy,
            'export_ready' => $withExports,
            'snapshot_refs' => array_values((array)($report['snapshot_refs'] ?? [])),
            'summary' => [
                'cases' => (int)($summary['case_count'] ?? 0),
                'critical' => (int)($summary['critical_count'] ?? 0),
                'high' => (int)($summary['high_count'] ?? 0),
                'medium' => (int)($summary['medium_count'] ?? 0),
                'associated_inventory_cost' => round((float)($summary['inventory_cost_abs'] ?? 0), 2),
                'movement_rows' => count((array)($report['movement_details'] ?? [])),
            ],
            'files' => [
                'page_json' => $this->fileMeta($dir . DIRECTORY_SEPARATOR . 'page.json'),
                'report_json' => $this->fileMeta($dir . DIRECTORY_SEPARATOR . 'report.json'),
                'excel' => $withExports ? $this->fileMeta($excelPath) : null,
                'powerpoint' => $withExports ? $this->fileMeta($pptPath) : null,
            ],
            'contract' => [
                'excel_sheets' => 12,
                'excel_charts' => 4,
                'powerpoint_slides' => 12,
                'powerpoint_ratio' => '16:9',
                'operational_mm_writes' => 'NONE',
                'frozen_table_writes' => 'NONE',
                'database_link' => 'NONE',
            ],
        ];
        $this->writeJsonAtomic($dir . DIRECTORY_SEPARATOR . 'manifest.json', $manifest);
    }

    private function validateReport(array $report, string $scope): void
    {
        $summary = (array)($report['summary'] ?? []);
        $caseCount = count((array)($report['cases'] ?? []));
        if ($caseCount !== (int)($summary['case_count'] ?? $summary['detected_count'] ?? $caseCount)) {
            throw new RuntimeException('DIAR_REPORT_CACHE_CASE_TOTAL_MISMATCH');
        }
        if (strtoupper((string)($report['month_status'] ?? '')) !== 'CLOSED') {
            throw new RuntimeException('DIAR_REPORT_CACHE_MONTH_NOT_CLOSED');
        }
        if (strtoupper((string)($report['scope_code'] ?? '')) !== $scope) {
            throw new RuntimeException('DIAR_REPORT_CACHE_REPORT_SCOPE_MISMATCH');
        }
        foreach ((array)($report['cases'] ?? []) as $case) {
            if (in_array((int)($case['store_code'] ?? -1), [56, 57], true)) {
                throw new RuntimeException('DIAR_REPORT_CACHE_EXCLUDED_STORE_FOUND');
            }
        }
    }

    private function signature(array $report, string $scope, string $month, bool $withExports): string
    {
        $identity = [
            'release' => self::RELEASE,
            'scope' => $scope,
            'month' => $month,
            'mode' => $withExports ? 'FULL' : 'PAGE',
            'snapshot_refs' => array_values((array)($report['snapshot_refs'] ?? [])),
            'summary' => (array)($report['summary'] ?? []),
            'workbook_sha256' => DiarReportCatalog::APPROVED_WORKBOOK_SHA256,
        ];
        $json = json_encode($identity, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        return substr(hash('sha256', (string)$json), 0, 24);
    }

    private function resolveScope(array $allowedSites, string $requested): string
    {
        $allowed = array_values(array_intersect(['NJCH', 'MNH'], array_map('strtoupper', $allowedSites)));
        if ($allowed === []) {
            throw new RuntimeException('DIAR_REPORT_CACHE_NO_ALLOWED_SITE');
        }
        $requested = strtoupper(trim($requested));
        if ($requested === 'ALL') {
            if (in_array('NJCH', $allowed, true) && in_array('MNH', $allowed, true)) {
                return 'ALL';
            }
            return (string)$allowed[0];
        }
        if (in_array($requested, $allowed, true)) {
            return $requested;
        }
        if (in_array('NJCH', $allowed, true)) {
            return 'NJCH';
        }
        return (string)$allowed[0];
    }

    private function scopeAllowed(array $allowedSites, string $scope): bool
    {
        $allowed = array_values(array_intersect(['NJCH', 'MNH'], array_map('strtoupper', $allowedSites)));
        if ($scope === 'ALL') {
            return in_array('NJCH', $allowed, true) && in_array('MNH', $allowed, true);
        }
        return in_array($scope, $allowed, true);
    }

    private function normaliseMonth(string $month): string
    {
        $month = trim($month);
        return preg_match('/^[0-9]{4}-[0-9]{2}$/', $month) === 1 ? $month : '';
    }

    private function pointerPath(string $scope, string $month): string
    {
        return $this->root() . DIRECTORY_SEPARATOR . 'pointers' . DIRECTORY_SEPARATOR
            . strtolower($scope . '_' . $month) . '.json';
    }

    private function root(): string
    {
        $root = base_path(self::STORAGE_RELATIVE);
        $this->ensureDirectory($root);
        return $root;
    }

    private function ensureDirectory(string $path): void
    {
        if (!is_dir($path) && !mkdir($path, 0777, true) && !is_dir($path)) {
            throw new RuntimeException('DIAR_REPORT_CACHE_DIRECTORY_FAILED|' . $path);
        }
    }

    /** @return array<string,mixed> */
    private function readJson(string $path): array
    {
        if (!is_file($path)) {
            return [];
        }
        $data = json_decode((string)file_get_contents($path), true);
        return is_array($data) ? $data : [];
    }

    private function writeJsonAtomic(string $path, array $data): void
    {
        $this->ensureDirectory(dirname($path));
        $json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        if ($json === false) {
            throw new RuntimeException('DIAR_REPORT_CACHE_JSON_ENCODE_FAILED');
        }
        $temp = $path . '.tmp_' . substr(bin2hex(random_bytes(4)), 0, 8);
        if (file_put_contents($temp, $json, LOCK_EX) === false) {
            @unlink($temp);
            throw new RuntimeException('DIAR_REPORT_CACHE_JSON_WRITE_FAILED|' . $path);
        }
        // Windows cannot reliably rename a file over an existing destination.
        if (is_file($path) && !@unlink($path)) {
            @unlink($temp);
            throw new RuntimeException('DIAR_REPORT_CACHE_POINTER_REPLACE_FAILED|' . $path);
        }
        if (!@rename($temp, $path)) {
            @unlink($temp);
            throw new RuntimeException('DIAR_REPORT_CACHE_JSON_PROMOTE_FAILED|' . $path);
        }
    }

    /** @return array<string,mixed> */
    private function fileMeta(string $path): array
    {
        if (!is_file($path)) {
            throw new RuntimeException('DIAR_REPORT_CACHE_FILE_MISSING|' . basename($path));
        }
        return [
            'name' => basename($path),
            'size' => (int)filesize($path),
            'sha256' => strtolower((string)hash_file('sha256', $path)),
        ];
    }

    private function cleanupOldGenerations(string $scope, string $month, string $keepSignature): void
    {
        $dir = $this->root() . DIRECTORY_SEPARATOR . 'generations' . DIRECTORY_SEPARATOR . $scope
            . DIRECTORY_SEPARATOR . $month;
        $rows = glob($dir . DIRECTORY_SEPARATOR . '*', GLOB_ONLYDIR) ?: [];
        usort($rows, static function (string $a, string $b): int {
            return (int)@filemtime($b) <=> (int)@filemtime($a);
        });
        $protected = [$keepSignature];
        $latestPointer = $this->readJson($this->pointerPath($scope, 'latest'));
        $latestGeneration = trim((string)($latestPointer['generation'] ?? ''));
        if ($latestGeneration !== '') {
            $protected[] = basename(str_replace('\\', '/', $latestGeneration));
        }
        $kept = 0;
        foreach ($rows as $row) {
            if (in_array(basename($row), $protected, true) || $kept < self::KEEP_GENERATIONS) {
                $kept++;
                continue;
            }
            $this->removeTree($row);
        }
    }

    private function removeTree(string $path): void
    {
        if (!is_dir($path)) {
            @unlink($path);
            return;
        }
        $items = scandir($path);
        if (!is_array($items)) {
            return;
        }
        foreach ($items as $item) {
            if ($item === '.' || $item === '..') {
                continue;
            }
            $child = $path . DIRECTORY_SEPARATOR . $item;
            if (is_dir($child)) {
                $this->removeTree($child);
            } else {
                @unlink($child);
            }
        }
        @rmdir($path);
    }
}
