<?php

declare(strict_types=1);

namespace App\Services;

use PharData;
use RuntimeException;
use Throwable;
use ZipArchive;

final class PowerBiDailyRevenueExportService
{
    private const MASTER_PBIX_RELATIVE_PATH = 'resources/powerbi/Daily_Inpatient_Revenue_Master.pbix';
    private const LEGACY_NJCH_PBIX_RELATIVE_PATH = 'resources/powerbi/NJCH_Daily_Revenue.pbix';
    private const MASTER_PBIX_PACKAGE_NAME = 'Daily_Inpatient_Revenue_Master.pbix';

    /** @param array<string,mixed> $report */
    public function stream(array $report, string $filename): void
    {
        $tmpBase = tempnam(sys_get_temp_dir(), 'ddb_pbi_');
        if ($tmpBase === false) { throw new RuntimeException('Unable to create export file.'); }
        @unlink($tmpBase);
        $tmp = $tmpBase . '.zip';
        try {
            $this->createPackage($tmp, $report);
            header('Content-Type: application/zip');
            header('Content-Disposition: attachment; filename="' . preg_replace('/[^A-Za-z0-9._-]/', '_', $filename) . '"');
            header('Content-Length: ' . (string)filesize($tmp));
            header('Cache-Control: private, max-age=0, must-revalidate');
            readfile($tmp);
        } finally { @unlink($tmp); }
    }

    /** @param array<string,mixed> $report */
    public function create(string $path, array $report): void
    {
        $this->writeZipPackage($path, $this->dataPackEntries($report));
    }

    /** @param array<string,mixed> $report */
    public function createPackage(string $path, array $report): void
    {
        $entries = [];
        foreach ($this->dataPackEntries($report) as $name => $content) {
            $entries['DataPack/' . $name] = $content;
        }

        $pbix = $this->findPowerBiReportFile();
        if ($pbix !== null) {
            $content = @file_get_contents($pbix['path']);
            if ($content === false) { throw new RuntimeException('Unable to read Power BI report file.'); }
            $entries[self::MASTER_PBIX_PACKAGE_NAME] = $content;
        } else {
            $entries['PBIX_NOT_INCLUDED.txt'] = "A Power BI Desktop report file was not found on the server.

"
                . "To include the executive report, create and validate the report in Power BI Desktop, then place it here:
"
                . self::MASTER_PBIX_RELATIVE_PATH . "

"
                . "Legacy fallback still supported for transition only:
"
                . self::LEGACY_NJCH_PBIX_RELATIVE_PATH . "

"
                . "The application does not fabricate PBIX files. This package still contains a complete DataPack folder for Power BI Desktop refresh.
";
        }
        $this->writeZipPackage($path, $entries);
    }

    /** @param array<string,mixed> $report @return array<string,string> */
    private function dataPackEntries(array $report): array
    {
        $summary = is_array($report['summary'] ?? null) ? $report['summary'] : [];
        $companies = array_values(array_filter((array)($report['companies'] ?? []), 'is_array'));
        $totalRevenue = (float)($summary['daily_revenue'] ?? 0);
        $topPayerRevenue = isset($companies[0]) && is_array($companies[0]) ? (float)($companies[0]['daily_revenue'] ?? 0) : 0.0;
        $largestPayerShare = $totalRevenue > 0 ? round(($topPayerRevenue / $totalRevenue) * 100, 4) : 0.0;
        $entries = [
            'summary.csv' => $this->csv([
                ['metric','value'],
                ['hospital_code', strtoupper((string)($report['hospital_code'] ?? ''))],
                ['hospital_name', (string)($report['hospital_name'] ?? '')],
                ['generated_at', (string)($summary['generated_at'] ?? '')],
                ['patient_count', (int)($summary['patient_count'] ?? 0)],
                ['daily_revenue', $this->num($summary['daily_revenue'] ?? 0)],
                ['average_revenue', $this->num($summary['average_revenue'] ?? 0)],
                ['largest_payer_share_pct', $largestPayerShare],
                ['review_count', (int)($summary['review_count'] ?? 0)],
            ]),
            'payer_concentration.csv' => $this->csv(array_merge([
                ['financial_class','patients','daily_revenue','share_pct'],
            ], $this->payerRows($report))),
            'care_area_revenue.csv' => $this->csv(array_merge([
                ['unit_name','patients','beds','occupancy_pct','daily_revenue','average_revenue'],
            ], $this->unitRows($report))),
            'patient_level_detail.csv' => $this->csv(array_merge([
                ['patient_id','patient_name','attending_doctor','location','unit_name','financial_class_id','financial_class','los','total_los','accumulated_share','daily_revenue','status'],
            ], $this->patientRows($report))),
            'power_bi_manifest.json' => json_encode([
                'type' => 'Power BI Data Pack',
                'created_at' => date('c'),
                'hospital_code' => strtoupper((string)($report['hospital_code'] ?? '')),
                'hospital_name' => (string)($report['hospital_name'] ?? ''),
                'files' => ['summary.csv','payer_concentration.csv','care_area_revenue.csv','patient_level_detail.csv'],
                'expected_pbix_name' => self::MASTER_PBIX_PACKAGE_NAME,
                'expected_server_path' => self::MASTER_PBIX_RELATIVE_PATH,
                'legacy_server_path' => self::LEGACY_NJCH_PBIX_RELATIVE_PATH,
                'pbix_report' => $this->findPowerBiReportFile() !== null ? 'included' : 'not_included',
            ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES),
            'README.txt' => "Daily Inpatient Revenue — Power BI Package

"
                . "Open " . self::MASTER_PBIX_PACKAGE_NAME . " in Power BI Desktop when it is included in the package, then refresh from the DataPack folder.

"
                . "If " . self::MASTER_PBIX_PACKAGE_NAME . " is not included, the DataPack folder is still complete and can be used to build or refresh the Master PBIX manually.

"
                . "Expected server master report path: " . self::MASTER_PBIX_RELATIVE_PATH . "
"
                . "This application does not create or fake PBIX files.
",
        ];
        return $entries;
    }

    /** @return array{path:string,relative:string}|null */
    private function findPowerBiReportFile(): ?array
    {
        $candidates = [
            self::MASTER_PBIX_RELATIVE_PATH,
            self::LEGACY_NJCH_PBIX_RELATIVE_PATH,
        ];

        foreach ($candidates as $relative) {
            $path = base_path($relative);
            if (!is_file($path)) { continue; }
            $size = @filesize($path);
            if ($size === false || $size <= 0) { continue; }

            $handle = @fopen($path, 'rb');
            if ($handle === false) { continue; }
            $signature = (string)fread($handle, 2);
            fclose($handle);

            // PBIX files saved by Power BI Desktop are ZIP containers. This small
            // gate prevents accidental inclusion of text placeholders renamed as .pbix.
            if ($signature !== 'PK') { continue; }

            return ['path' => $path, 'relative' => $relative];
        }

        return null;
    }

    /** @return list<list<mixed>> */
    private function payerRows(array $report): array
    {
        $summary = is_array($report['summary'] ?? null) ? $report['summary'] : [];
        $total = (float)($summary['daily_revenue'] ?? 0);
        $rows = [];
        foreach ((array)($report['companies'] ?? []) as $company) {
            if (!is_array($company)) { continue; }
            $value = (float)($company['daily_revenue'] ?? 0);
            $rows[] = [(string)($company['financial_class'] ?? ''), (int)($company['patients'] ?? 0), $this->num($value), $total > 0 ? round(($value / $total) * 100, 4) : 0];
        }
        return $rows;
    }

    /** @return list<list<mixed>> */
    private function unitRows(array $report): array
    {
        $rows = [];
        foreach ((array)($report['units'] ?? []) as $unit) {
            if (!is_array($unit)) { continue; }
            $patients = (int)($unit['patients'] ?? 0);
            $beds = (int)($unit['beds'] ?? 0);
            $revenue = (float)($unit['daily_revenue'] ?? 0);
            $rows[] = [(string)($unit['unit_name'] ?? ''), $patients, $beds, $beds > 0 ? round(($patients / $beds) * 100, 4) : 0, $this->num($revenue), $patients > 0 ? $this->num($revenue / $patients) : 0];
        }
        return $rows;
    }

    /** @return list<list<mixed>> */
    private function patientRows(array $report): array
    {
        $rows = [];
        foreach ((array)($report['patients'] ?? []) as $patient) {
            if (!is_array($patient)) { continue; }
            $review = !empty($patient['needs_review']);
            $share = !empty($patient['is_cash_payer']) ? (float)($patient['patient_share_total'] ?? 0) : (float)($patient['company_share_total'] ?? 0);
            $rows[] = [
                (string)($patient['patientid'] ?? ''),
                (string)($patient['patient_name'] ?? ''),
                (string)($patient['attending_doctor'] ?? ''),
                trim((string)($patient['room'] ?? '') . ' / ' . (string)($patient['bed'] ?? '')),
                (string)($patient['unit_name'] ?? ''),
                (string)($patient['financlassid'] ?? ''),
                (string)($patient['financial_class'] ?? ''),
                (int)($patient['los'] ?? 0),
                (int)($patient['total_los'] ?? 0),
                $this->num($share),
                $this->num((float)($patient['daily_revenue'] ?? 0)),
                $review ? 'Needs review' : 'Calculated',
            ];
        }
        return $rows;
    }

    /** @param list<list<mixed>> $rows */
    private function csv(array $rows): string
    {
        $fp = fopen('php://temp', 'r+');
        if ($fp === false) { throw new RuntimeException('Unable to create CSV content.'); }
        foreach ($rows as $row) {
            fputcsv($fp, array_map(static function ($v) { return is_float($v) ? number_format($v, 4, '.', '') : $v; }, $row));
        }
        rewind($fp);
        $content = stream_get_contents($fp);
        fclose($fp);
        return "\xEF\xBB\xBF" . (string)$content;
    }

    private function num($value): float { return round((float)$value, 2); }

    /** @param array<string,string> $entries */
    private function writeZipPackage(string $path, array $entries): void
    {
        @unlink($path);
        if (class_exists(ZipArchive::class)) {
            $zip = new ZipArchive();
            if ($zip->open($path, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) { throw new RuntimeException('Unable to create Power BI data pack.'); }
            foreach ($entries as $name => $content) { $zip->addFromString($name, $content); }
            $zip->close();
            return;
        }
        if (class_exists(PharData::class)) {
            $zipPath = $path . '.zip';
            @unlink($zipPath);
            try {
                $archive = new PharData($zipPath);
                foreach ($entries as $name => $content) { $archive[$name] = $content; }
                unset($archive);
                if (!@rename($zipPath, $path)) { throw new RuntimeException('Unable to finalize Power BI data pack.'); }
                return;
            } catch (Throwable $e) {
                @unlink($zipPath); @unlink($path);
                throw new RuntimeException('Unable to create Power BI data pack.', 0, $e);
            }
        }
        throw new RuntimeException('A ZIP-capable PHP archive engine is required.');
    }
}
