<?php

declare(strict_types=1);

namespace App\Services;

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

final class PowerBiDailyRevenueExportService
{
    /** @var PowerBiReportValidator */
    private $validator;

    public function __construct(?PowerBiReportValidator $validator = null)
    {
        $this->validator = $validator !== null ? $validator : new PowerBiReportValidator();
    }

    /** @return array<string,mixed> */
    public function reportStatus(): array
    {
        return $this->validator->status();
    }

    public function reportAvailable(): bool
    {
        $status = $this->reportStatus();
        return ($status['available'] ?? false) === true;
    }

    /** @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);
        }
    }

    /**
     * Create the standalone DataPack ZIP. This method does not claim that a PBIX
     * exists and remains available for controlled QA and authoring workflows.
     *
     * @param array<string,mixed> $report
     */
    public function create(string $path, array $report): void
    {
        $status = $this->reportStatus();
        $this->writeZipPackage($path, $this->dataPackEntries($report, $status));
    }

    /**
     * Create the user-facing Power BI package. R40 is fail-closed: a PBIX is
     * included only after structural validation and an exact SHA-256-bound
     * Power BI Desktop acceptance receipt pass.
     *
     * @param array<string,mixed> $report
     */
    public function createPackage(string $path, array $report): void
    {
        $status = $this->reportStatus();
        if (($status['available'] ?? false) !== true) {
            throw new RuntimeException(
                'Power BI report is not available. State=' . (string)($status['state'] ?? 'UNKNOWN')
                . '; Reason=' . (string)($status['reason'] ?? 'UNKNOWN')
            );
        }

        $relative = (string)($status['selected_relative_path'] ?? '');
        if ($relative === '') {
            throw new RuntimeException('Power BI report status is inconsistent: no selected report path.');
        }

        $pbixPath = $this->validator->acceptedReportPath($status);
        if ($pbixPath === null) {
            throw new RuntimeException('Power BI report status is inconsistent: accepted report path unavailable.');
        }
        $content = @file_get_contents($pbixPath);
        if ($content === false) {
            throw new RuntimeException('Unable to read the accepted Power BI report file.');
        }

        $entries = [];
        foreach ($this->dataPackEntries($report, $status) as $name => $entryContent) {
            $entries['DataPack/' . $name] = $entryContent;
        }
        $entries[PowerBiReportValidator::PACKAGE_NAME] = $content;
        $entries['PBIX_VALIDATION.json'] = (string)json_encode(
            $this->publicValidationManifest($status),
            JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES
        );

        $this->writeZipPackage($path, $entries);
    }

    /** @param array<string,mixed> $report @param array<string,mixed> $status @return array<string,string> */
    private function dataPackEntries(array $report, array $status): 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;

        return [
            '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' => (string)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' => PowerBiReportValidator::PACKAGE_NAME,
                'expected_server_path' => PowerBiReportValidator::MASTER_RELATIVE_PATH,
                'legacy_server_path' => PowerBiReportValidator::LEGACY_RELATIVE_PATH,
                'pbix_gate_version' => PowerBiReportValidator::GATE_VERSION,
                'pbix_state' => (string)($status['state'] ?? 'UNKNOWN'),
                'pbix_available' => ($status['available'] ?? false) === true,
                'desktop_acceptance_required' => true,
            ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES),
            'README.txt' => "Daily Inpatient Revenue — Power BI DataPack\r\n\r\n"
                . "This DataPack contains the current Daily Inpatient Revenue export tables.\r\n\r\n"
                . "A user-facing Power BI package is released only when the exact PBIX file passes the R40 structural gate and has a valid SHA-256-bound Power BI Desktop acceptance receipt proving open, refresh, NJCH reconciliation, and MNH reconciliation.\r\n\r\n"
                . "Expected server master report path: " . PowerBiReportValidator::MASTER_RELATIVE_PATH . "\r\n"
                . "The application does not create, fake, or infer Power BI Desktop acceptance.\r\n",
        ];
    }

    /** @param array<string,mixed> $status @return array<string,mixed> */
    private function publicValidationManifest(array $status): array
    {
        return [
            'gate_version' => (string)($status['gate_version'] ?? PowerBiReportValidator::GATE_VERSION),
            'state' => (string)($status['state'] ?? 'UNKNOWN'),
            'reason' => (string)($status['reason'] ?? 'UNKNOWN'),
            'selected_relative_path' => (string)($status['selected_relative_path'] ?? ''),
            'package_name' => PowerBiReportValidator::PACKAGE_NAME,
            'size_bytes' => (int)($status['size_bytes'] ?? 0),
            'sha256' => (string)($status['sha256'] ?? ''),
            'desktop_acceptance_required' => true,
            'acceptance_receipt_relative_path' => (string)($status['acceptance_receipt_relative_path'] ?? ''),
            'checks' => array_values(array_filter((array)($status['checks'] ?? []), 'is_array')),
        ];
    }

    /** @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 ($value) {
                return is_float($value) ? number_format($value, 4, '.', '') : $value;
            }, $row));
        }
        rewind($fp);
        $content = stream_get_contents($fp);
        fclose($fp);
        return "\xEF\xBB\xBF" . (string)$content;
    }

    /** @param mixed $value */
    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 package.');
            }
            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 package.');
                }
                return;
            } catch (Throwable $e) {
                @unlink($zipPath);
                @unlink($path);
                throw new RuntimeException('Unable to create Power BI package.', 0, $e);
            }
        }

        throw new RuntimeException('A ZIP-capable PHP archive engine is required.');
    }
}
