<?php

declare(strict_types=1);

namespace App\Controllers;

use App\Core\Auth;
use App\Core\Request;
use App\Core\View;
use App\Repositories\BedsRepository;
use App\Repositories\DoctorsRepository;
use App\Repositories\OperationsRepository;
use App\Repositories\VisitsRepository;
use App\Services\ChartDataService;
use DateTimeImmutable;
use Throwable;

final class OperationsController
{
    private function modules(): array
    {
        return [
            'visits' => [
                'icon' => '◇',
                'tone' => 'blue',
                'title_key' => 'operations.visits.title',
                'hero_key' => 'operations.visits.hero_title',
                'text_key' => 'operations.visits.hero_text',
            ],
            'doctors' => [
                'icon' => '✚',
                'tone' => 'green',
                'title_key' => 'operations.doctors.title',
                'hero_key' => 'operations.doctors.hero_title',
                'text_key' => 'operations.doctors.hero_text',
            ],
            'beds' => [
                'icon' => '▣',
                'tone' => 'violet',
                'title_key' => 'operations.beds.title',
                'hero_key' => 'operations.beds.hero_title',
                'text_key' => 'operations.beds.hero_text',
            ],
            'operations' => [
                'icon' => '✦',
                'tone' => 'blue',
                'title_key' => 'operations.operations.title',
                'hero_key' => 'operations.operations.hero_title',
                'text_key' => 'operations.operations.hero_text',
            ],
        ];
    }

    public function module(string $module): void
    {
        Auth::requireLogin();

        $module = strtolower(trim($module));
        $modules = $this->modules();
        if (!array_key_exists($module, $modules)) {
            \redirect('index.php?page=dashboard&lang=' . \current_lang());
        }

        $hospitalCode = Auth::hospital();
        $hospital = \config('hospitals.' . $hospitalCode, []);
        $config = $modules[$module];

        $currentYear = (int) date('Y');
        $yearWasRequested = Request::get('year') !== null;
        $compareYearWasRequested = Request::get('compare_year') !== null;
        $year = \safe_int(Request::get('year'), $currentYear);
        $compareYear = \safe_int(Request::get('compare_year'), $year - 1);
        $metric = strtoupper(trim((string)Request::get('metric', 'TOT_REV')));

        $reportMode = 'year';
        $fromDate = sprintf('%04d-01-01', $year);
        $toDate = sprintf('%04d-03-31', $year);
        $visitsFilterError = null;
        $operationsFilterError = null;
        if (in_array($module, ['visits', 'operations'], true)) {
            $requestedMode = strtolower(trim((string)Request::get('mode', 'year')));
            $reportMode = in_array($requestedMode, ['year', 'range'], true) ? $requestedMode : 'year';
            $fromDate = trim((string)Request::get('from_date', $fromDate));
            $toDate = trim((string)Request::get('to_date', $toDate));

            if ($reportMode === 'range') {
                $from = DateTimeImmutable::createFromFormat('!Y-m-d', $fromDate);
                $fromErrors = DateTimeImmutable::getLastErrors();
                $fromOk = $from instanceof DateTimeImmutable && ($fromErrors === false || ((int)$fromErrors['warning_count'] === 0 && (int)$fromErrors['error_count'] === 0));
                $to = DateTimeImmutable::createFromFormat('!Y-m-d', $toDate);
                $toErrors = DateTimeImmutable::getLastErrors();
                $toOk = $to instanceof DateTimeImmutable && ($toErrors === false || ((int)$toErrors['warning_count'] === 0 && (int)$toErrors['error_count'] === 0));

                if (!$fromOk || !$toOk || $from > $to || $from->format('Y') !== $to->format('Y')) {
                    if ($module === 'operations') {
                        $operationsFilterError = \t('operations.invalid_range');
                    } else {
                        $visitsFilterError = \t('visits.invalid_range');
                    }
                    $reportMode = 'year';
                    $fromDate = sprintf('%04d-01-01', $year);
                    $toDate = sprintf('%04d-03-31', $year);
                } else {
                    $year = (int)$from->format('Y');
                    if (!$compareYearWasRequested) {
                        $compareYear = $year - 1;
                    }
                }
            }
        }

        if ($module === 'doctors') {
            try {
                $yearRepo = new DoctorsRepository($hospitalCode);
                $availableDoctorYears = $yearRepo->availableYears('TOT_REV', 20);
                if (!$yearWasRequested && !empty($availableDoctorYears)) {
                    $year = (int)$availableDoctorYears[0];
                }
                if (!$compareYearWasRequested && !empty($availableDoctorYears)) {
                    $compareYear = $this->defaultCompareYear($availableDoctorYears, $year);
                }
            } catch (Throwable $e) {
                $this->logWorkspaceError('Doctors year auto-detect warning', $e);
            }
        }

        if ($module === 'beds') {
            try {
                $yearRepo = new BedsRepository($hospitalCode);
                $availableBedYears = $yearRepo->availableYears(20);
                if (!$yearWasRequested && !empty($availableBedYears)) {
                    $year = (int)$availableBedYears[0];
                }
                if (!$compareYearWasRequested && !empty($availableBedYears)) {
                    $compareYear = $this->defaultCompareYear($availableBedYears, $year);
                }
            } catch (Throwable $e) {
                $this->logWorkspaceError('Beds year auto-detect warning', $e);
            }
        }

        if ($module === 'operations' && $reportMode === 'year') {
            try {
                $yearRepo = new OperationsRepository($hospitalCode);
                $availableOperationYears = $yearRepo->availableYears(20);
                if (!$yearWasRequested && !empty($availableOperationYears)) {
                    $year = (int)$availableOperationYears[0];
                }
                if (!$compareYearWasRequested && !empty($availableOperationYears)) {
                    $compareYear = $this->defaultCompareYear($availableOperationYears, $year);
                }
                $fromDate = sprintf('%04d-01-01', $year);
                $toDate = sprintf('%04d-03-31', $year);
            } catch (Throwable $e) {
                $this->logWorkspaceError('Operations year auto-detect warning', $e);
            }
        }

        $stats = [
            ['label' => \t('operations.stat_branch'), 'value' => (string)($hospital['name'] ?? strtoupper($hospitalCode)), 'hint' => \t('operations.stat_branch_hint'), 'tone' => 'blue'],
            ['label' => \t('operations.stat_connection'), 'value' => \t('operations.stat_connection_value'), 'hint' => \t('operations.stat_connection_hint'), 'tone' => 'green'],
            ['label' => \t('operations.stat_security'), 'value' => \t('operations.stat_security_value'), 'hint' => \t('operations.stat_security_hint'), 'tone' => 'violet'],
            ['label' => \t('operations.stat_next'), 'value' => $this->statusValue($module), 'hint' => $this->statusHint($module), 'tone' => 'amber'],
        ];

        if (in_array($module, ['visits', 'operations'], true)) {
            $periodRepo = $module === 'operations' ? OperationsRepository::class : VisitsRepository::class;
            $prefix = $module === 'operations' ? 'operations' : 'visits';
            $currentPeriod = $reportMode === 'range' ? ($fromDate . ' - ' . $toDate) : (string)$year;
            $comparePeriod = $reportMode === 'range'
                ? ($periodRepo::shiftDateToYear($fromDate, $compareYear) . ' - ' . $periodRepo::shiftDateToYear($toDate, $compareYear))
                : (string)$compareYear;
            $stats = [
                ['label' => \t($prefix . '.stat_period_mode'), 'value' => \t($prefix . '.' . ($reportMode === 'range' ? 'mode_range' : 'mode_year')), 'hint' => \t($prefix . '.stat_period_mode_hint'), 'tone' => 'blue'],
                ['label' => \t($prefix . '.stat_current_period'), 'value' => $currentPeriod, 'hint' => \t($prefix . '.stat_current_period_hint'), 'tone' => 'green'],
                ['label' => \t($prefix . '.stat_compare_period'), 'value' => $comparePeriod, 'hint' => \t($prefix . '.stat_compare_period_hint'), 'tone' => 'violet'],
                ['label' => \t($prefix . '.stat_branch'), 'value' => (string)($hospital['name'] ?? strtoupper($hospitalCode)), 'hint' => \t($prefix . '.stat_branch_hint'), 'tone' => 'amber'],
            ];
        }

        $visitData = null;
        $operationData = null;
        $doctorData = null;
        $bedsData = null;
        $dataError = null;

        if ($module === 'visits') {
            try {
                $visitData = $this->buildVisitsData($hospitalCode, $year, $compareYear, $reportMode, $fromDate, $toDate);
            } catch (Throwable $e) {
                $dataError = $e->getMessage();
                $this->logWorkspaceError('Visits workspace error', $e);
            }
        }

        if ($module === 'operations') {
            try {
                $operationData = $this->buildOperationsData($hospitalCode, $year, $compareYear, $reportMode, $fromDate, $toDate);
            } catch (Throwable $e) {
                $dataError = $e->getMessage();
                $this->logWorkspaceError('Operations workspace error', $e);
            }
        }

        if ($module === 'doctors') {
            try {
                $doctorData = $this->buildDoctorsData($hospitalCode, $year, $compareYear, $metric);
            } catch (Throwable $e) {
                $dataError = $e->getMessage();
                $this->logWorkspaceError('Doctors workspace error', $e);
            }
        }

        if ($module === 'beds') {
            try {
                $bedsData = $this->buildBedsData($hospitalCode, $year, $compareYear);
            } catch (Throwable $e) {
                $dataError = $e->getMessage();
                $this->logWorkspaceError('Beds workspace error', $e);
            }
        }

        View::render('operations/module', [
            'title' => \t($config['title_key']),
            'activePage' => $module,
            'module' => $module,
            'moduleConfig' => $config,
            'hospital' => $hospital,
            'hospitalCode' => $hospitalCode,
            'stats' => $stats,
            'modules' => $modules,
            'year' => $year,
            'compareYear' => $compareYear,
            'reportMode' => $reportMode,
            'fromDate' => $fromDate,
            'toDate' => $toDate,
            'visitsFilterError' => $visitsFilterError,
            'operationsFilterError' => $operationsFilterError,
            'metric' => $metric,
            'visitData' => $visitData,
            'operationData' => $operationData,
            'doctorData' => $doctorData,
            'bedsData' => $bedsData,
            'dataError' => $dataError,
        ]);
    }


    /**
     * @param array<int, int> $years
     */
    private function defaultCompareYear(array $years, int $year): int
    {
        foreach ($years as $candidate) {
            $candidate = (int)$candidate;
            if ($candidate > 0 && $candidate < $year) {
                return $candidate;
            }
        }
        return $year - 1;
    }

    private function statusValue(string $module): string
    {
        if ($module === 'visits') {
            return \t('operations.stat_live_data_value');
        }
        if ($module === 'doctors') {
            return \t('operations.stat_doctors_live_value');
        }
        if ($module === 'beds') {
            return \t('operations.stat_beds_live_value');
        }
        if ($module === 'operations') {
            return \t('operations.stat_operations_live_value');
        }
        return \t('operations.stat_next_value');
    }

    private function statusHint(string $module): string
    {
        if ($module === 'visits') {
            return \t('operations.stat_live_data_hint');
        }
        if ($module === 'doctors') {
            return \t('operations.stat_doctors_live_hint');
        }
        if ($module === 'beds') {
            return \t('operations.stat_beds_live_hint');
        }
        if ($module === 'operations') {
            return \t('operations.stat_operations_live_hint');
        }
        return \t('operations.stat_next_hint');
    }

    private function buildVisitsData(
        string $hospitalCode,
        int $year,
        int $compareYear,
        string $reportMode,
        string $fromDate,
        string $toDate
    ): array {
        $repo = new VisitsRepository($hospitalCode);
        $chartService = new ChartDataService();
        $warnings = [];
        $emptyMonthly = $repo->emptyMonthlyRows();
        $rangeFrom = $reportMode === 'range' ? $fromDate : null;
        $rangeTo = $reportMode === 'range' ? $toDate : null;

        $opdRows = $this->safeDataset('OPD monthly visits', function () use ($repo, $year, $compareYear, $rangeFrom, $rangeTo) {
            return $repo->monthlyOutpatientVisits($year, $compareYear, $rangeFrom, $rangeTo);
        }, $emptyMonthly, $warnings, 'Visits');

        $erRows = $this->safeDataset('Emergency monthly visits', function () use ($repo, $year, $compareYear, $rangeFrom, $rangeTo) {
            return $repo->monthlyEmergencyVisits($year, $compareYear, $rangeFrom, $rangeTo);
        }, $emptyMonthly, $warnings, 'Visits');

        $admissionRows = $this->safeDataset('Admissions monthly visits', function () use ($repo, $year, $compareYear, $rangeFrom, $rangeTo) {
            return $repo->monthlyAdmissions($year, $compareYear, $rangeFrom, $rangeTo);
        }, $emptyMonthly, $warnings, 'Visits');

        $followupRows = $this->safeDataset('New vs follow-up visits', function () use ($repo, $year, $rangeFrom, $rangeTo) {
            return $repo->followupMix($year, $rangeFrom, $rangeTo);
        }, [], $warnings, 'Visits');

        $admissionCategoryRows = $this->safeDataset('Admissions by payer category', function () use ($repo, $year, $rangeFrom, $rangeTo) {
            return $repo->admissionCategoryMix($year, $rangeFrom, $rangeTo);
        }, [], $warnings, 'Visits');

        $outpatientCashCreditRows = $this->safeDataset('Today outpatient payer mix', function () use ($repo) {
            return $repo->outpatientCashCreditToday();
        }, [], $warnings, 'Visits');

        $dailyRows = $this->safeDataset('Daily visits trend', function () use ($repo) {
            return $repo->dailyOutpatientTrend(14);
        }, [], $warnings, 'Visits');

        $specialties = $this->safeDataset('Top specialties', function () use ($repo, $year) {
            return $repo->topSpecialtiesByOpVisits($year);
        }, [], $warnings, 'Visits');

        $currentPeriodLabel = $reportMode === 'range' ? ($fromDate . ' - ' . $toDate) : (string)$year;
        $comparePeriodLabel = $reportMode === 'range'
            ? (VisitsRepository::shiftDateToYear($fromDate, $compareYear) . ' - ' . VisitsRepository::shiftDateToYear($toDate, $compareYear))
            : (string)$compareYear;

        $cards = [
            'opd' => $this->metricCardForPeriod(\t('visits.card_opd'), $opdRows, $currentPeriodLabel, $comparePeriodLabel, $chartService),
            'er' => $this->metricCardForPeriod(\t('visits.card_er'), $erRows, $currentPeriodLabel, $comparePeriodLabel, $chartService),
            'admissions' => $this->metricCardForPeriod(\t('visits.card_admissions'), $admissionRows, $currentPeriodLabel, $comparePeriodLabel, $chartService),
        ];

        $followupSummary = $this->followupSummary($followupRows);
        $cards['followup'] = [
            'title' => \t('visits.card_followup'),
            'current_total' => $followupSummary['followup'],
            'compare_total' => $followupSummary['new'],
            'percent' => $followupSummary['ratio'],
            'hint' => \t('visits.followup_hint'),
            'mode' => 'ratio',
        ];

        return [
            'mode' => $reportMode,
            'current_period_label' => $currentPeriodLabel,
            'compare_period_label' => $comparePeriodLabel,
            'charts' => [
                'opd' => [
                    'title' => \t('visits.chart_opd'),
                    'rows' => $opdRows,
                    'payload' => $chartService->comparePayload($opdRows, \t('visits.chart_opd'), $currentPeriodLabel, $comparePeriodLabel),
                ],
                'er' => [
                    'title' => \t('visits.chart_er'),
                    'rows' => $erRows,
                    'payload' => $chartService->comparePayload($erRows, \t('visits.chart_er'), $currentPeriodLabel, $comparePeriodLabel),
                ],
                'admissions' => [
                    'title' => \t('visits.chart_admissions'),
                    'rows' => $admissionRows,
                    'payload' => $chartService->comparePayload($admissionRows, \t('visits.chart_admissions'), $currentPeriodLabel, $comparePeriodLabel),
                ],
            ],
            'cards' => $cards,
            'followup' => $followupSummary,
            'admissionCategories' => $this->normalizeAdmissionCategories($admissionCategoryRows),
            'outpatientPayerMix' => $this->normalizeAdmissionCategories($outpatientCashCreditRows),
            'daily' => $this->normalizeDailyRows($dailyRows),
            'specialties' => $this->normalizeSpecialties($specialties),
            'warnings' => $warnings,
        ];
    }

    private function buildOperationsData(
        string $hospitalCode,
        int $year,
        int $compareYear,
        string $reportMode,
        string $fromDate,
        string $toDate
    ): array {
        $repo = new OperationsRepository($hospitalCode);
        $chartService = new ChartDataService();
        $warnings = [];
        $emptyMonthly = $repo->emptyMonthlyRows();
        $rangeFrom = $reportMode === 'range' ? $fromDate : null;
        $rangeTo = $reportMode === 'range' ? $toDate : null;

        $scheduledRows = $this->safeDataset('Scheduled operations', function () use ($repo, $year, $compareYear, $rangeFrom, $rangeTo) {
            return $repo->monthlyLegacyOperations($year, $compareYear, $rangeFrom, $rangeTo);
        }, $emptyMonthly, $warnings, 'Operations');

        $performedRows = $this->safeDataset('Performed operations', function () use ($repo, $year, $compareYear, $rangeFrom, $rangeTo) {
            return $repo->monthlyPerformedOperations($year, $compareYear, $rangeFrom, $rangeTo);
        }, $emptyMonthly, $warnings, 'Operations');

        $cancelledRows = $this->safeDataset('Cancelled operations', function () use ($repo, $year, $compareYear, $rangeFrom, $rangeTo) {
            return $repo->monthlyCancelledOperations($year, $compareYear, $rangeFrom, $rangeTo);
        }, $emptyMonthly, $warnings, 'Operations');

        $currentPeriodLabel = $reportMode === 'range' ? ($fromDate . ' - ' . $toDate) : (string)$year;
        $comparePeriodLabel = $reportMode === 'range'
            ? (OperationsRepository::shiftDateToYear($fromDate, $compareYear) . ' - ' . OperationsRepository::shiftDateToYear($toDate, $compareYear))
            : (string)$compareYear;

        $scheduledCurrent = $chartService->total($scheduledRows, 'current_value');
        $scheduledCompare = $chartService->total($scheduledRows, 'compare_value');
        $performedCurrent = $chartService->total($performedRows, 'current_value');
        $performedCompare = $chartService->total($performedRows, 'compare_value');
        $cancelledCurrent = $chartService->total($cancelledRows, 'current_value');
        $cancelledCompare = $chartService->total($cancelledRows, 'compare_value');
        $completionCurrent = $scheduledCurrent > 0 ? ($performedCurrent / $scheduledCurrent) * 100 : null;
        $completionCompare = $scheduledCompare > 0 ? ($performedCompare / $scheduledCompare) * 100 : null;

        $topSpecialties = $this->safeDataset('Top operation specialties', function () use ($repo, $year, $rangeFrom, $rangeTo) {
            return $repo->topSpecialties($year, $rangeFrom, $rangeTo, 8);
        }, [], $warnings, 'Operations');
        $topDoctors = $this->safeDataset('Top operation doctors', function () use ($repo, $year, $rangeFrom, $rangeTo) {
            return $repo->topDoctors($year, $rangeFrom, $rangeTo, 8);
        }, [], $warnings, 'Operations');
        $topRooms = $this->safeDataset('Top operating rooms', function () use ($repo, $year, $rangeFrom, $rangeTo) {
            return $repo->topOperatingRooms($year, $rangeFrom, $rangeTo, 8);
        }, [], $warnings, 'Operations');
        $availableYears = $this->safeDataset('Operations available years', function () use ($repo) {
            return $repo->availableYears(20);
        }, [], $warnings, 'Operations');

        $cards = [
            'scheduled' => $this->metricCardForPeriod(\t('operations.card_scheduled'), $scheduledRows, $currentPeriodLabel, $comparePeriodLabel, $chartService),
            'performed' => $this->metricCardForPeriod(\t('operations.card_performed'), $performedRows, $currentPeriodLabel, $comparePeriodLabel, $chartService),
            'cancelled' => $this->metricCardForPeriod(\t('operations.card_cancelled'), $cancelledRows, $currentPeriodLabel, $comparePeriodLabel, $chartService),
            'completion' => [
                'title' => \t('operations.card_completion'),
                'current_total' => $completionCurrent,
                'compare_total' => $completionCompare,
                'percent' => ($completionCurrent !== null && $completionCompare !== null) ? ($completionCurrent - $completionCompare) : null,
                'hint' => \t('operations.completion_hint'),
                'mode' => 'percent',
            ],
        ];

        return [
            'mode' => $reportMode,
            'current_period_label' => $currentPeriodLabel,
            'compare_period_label' => $comparePeriodLabel,
            'cards' => $cards,
            'summary' => [
                'scheduled_current' => $scheduledCurrent,
                'scheduled_compare' => $scheduledCompare,
                'performed_current' => $performedCurrent,
                'performed_compare' => $performedCompare,
                'cancelled_current' => $cancelledCurrent,
                'cancelled_compare' => $cancelledCompare,
                'completion_rate_current' => $completionCurrent,
                'completion_rate_compare' => $completionCompare,
            ],
            'charts' => [
                'scheduled' => [
                    'title' => \t('operations.chart_scheduled'),
                    'rows' => $scheduledRows,
                    'payload' => $chartService->comparePayload($scheduledRows, \t('operations.chart_scheduled'), $currentPeriodLabel, $comparePeriodLabel),
                ],
                'performed' => [
                    'title' => \t('operations.chart_performed'),
                    'rows' => $performedRows,
                    'payload' => $chartService->comparePayload($performedRows, \t('operations.chart_performed'), $currentPeriodLabel, $comparePeriodLabel),
                ],
                'cancelled' => [
                    'title' => \t('operations.chart_cancelled'),
                    'rows' => $cancelledRows,
                    'payload' => $chartService->comparePayload($cancelledRows, \t('operations.chart_cancelled'), $currentPeriodLabel, $comparePeriodLabel),
                ],
            ],
            'topSpecialties' => $this->normalizeOperationRanks($topSpecialties, 'specialty_name'),
            'topDoctors' => $this->normalizeOperationRanks($topDoctors, 'doctor_name'),
            'topRooms' => $this->normalizeOperationRanks($topRooms, 'room_name'),
            'availableYears' => is_array($availableYears) ? array_map('intval', $availableYears) : [],
            'warnings' => $warnings,
        ];
    }

    private function buildDoctorsData(string $hospitalCode, int $year, int $compareYear, string $metric): array
    {
        $repo = new DoctorsRepository($hospitalCode);
        $chartService = new ChartDataService();
        $warnings = [];

        $availableMetrics = $repo->availableMetrics();
        $allowed = array_map(function (array $row): string { return (string)$row['key']; }, $availableMetrics);
        if (!in_array($metric, $allowed, true)) {
            $metric = in_array('TOT_REV', $allowed, true) ? 'TOT_REV' : (string)($allowed[0] ?? 'TOT_REV');
        }

        $availableYears = $this->safeDataset('Doctor available years', function () use ($repo, $metric) {
            return $repo->availableYears($metric, 20);
        }, [], $warnings, 'Doctors');
        $availableYears = is_array($availableYears) ? array_map('intval', $availableYears) : [];
        if (!empty($availableYears) && !in_array($year, $availableYears, true)) {
            $warnings[] = \t('doctors.no_data_for_selected_year', ['year' => $year, 'latest' => (int)$availableYears[0]]);
        }

        $summary = $this->safeDataset('Doctor summary', function () use ($repo, $year, $compareYear) {
            return $repo->summary($year, $compareYear);
        }, [], $warnings, 'Doctors');

        $topDoctors = $this->safeDataset('Top doctors', function () use ($repo, $year, $metric) {
            return $repo->topDoctors($year, $metric, 10);
        }, [], $warnings, 'Doctors');

        $specialties = $this->safeDataset('Specialty contribution', function () use ($repo, $year, $metric) {
            return $repo->specialties($year, $metric, 10);
        }, [], $warnings, 'Doctors');

        $mix = $this->safeDataset('Doctor revenue mix', function () use ($repo, $year) {
            return $repo->revenueMix($year);
        }, [], $warnings, 'Doctors');

        $summary = is_array($summary) ? $summary : [];
        $cards = [
            'revenue' => $this->doctorMetricCard(\t('doctors.card_revenue'), (float)($summary['current_revenue'] ?? 0), (float)($summary['compare_revenue'] ?? 0), $year, $compareYear, $chartService),
            'opd' => $this->doctorMetricCard(\t('doctors.card_opd'), (float)($summary['current_op_visits'] ?? 0), (float)($summary['compare_op_visits'] ?? 0), $year, $compareYear, $chartService),
            'inpatient' => $this->doctorMetricCard(\t('doctors.card_inpatient'), (float)($summary['current_inpat_visits'] ?? 0), (float)($summary['compare_inpat_visits'] ?? 0), $year, $compareYear, $chartService),
            'active' => [
                'title' => \t('doctors.card_active'),
                'current_total' => (float)($summary['active_doctors'] ?? 0),
                'compare_total' => (float)($summary['current_moh'] ?? 0),
                'percent' => null,
                'hint' => \t('doctors.active_hint'),
                'mode' => 'single',
            ],
        ];

        return [
            'summary' => $summary,
            'cards' => $cards,
            'topDoctors' => $topDoctors,
            'specialties' => $specialties,
            'mix' => $mix,
            'metric' => $metric,
            'availableMetrics' => $availableMetrics,
            'availableYears' => $availableYears,
            'warnings' => $warnings,
            'topDoctorsPayload' => $this->singleSeriesPayload(\t('doctors.top_doctors_title'), $topDoctors, $this->metricLabel($availableMetrics, $metric)),
            'specialtiesPayload' => $this->singleSeriesPayload(\t('doctors.specialty_title'), $specialties, $this->metricLabel($availableMetrics, $metric)),
        ];
    }


    private function buildBedsData(string $hospitalCode, int $year, int $compareYear): array
    {
        $repo = new BedsRepository($hospitalCode);
        $chartService = new ChartDataService();
        $warnings = [];

        $availableYears = $this->safeDataset('Beds available years', function () use ($repo) {
            return $repo->availableYears(20);
        }, [], $warnings, 'Beds');
        $availableYears = is_array($availableYears) ? array_map('intval', $availableYears) : [];
        if (!empty($availableYears) && !in_array($year, $availableYears, true)) {
            $warnings[] = \t('beds.no_data_for_selected_year', ['year' => $year, 'latest' => (int)$availableYears[0]]);
        }

        $current = $this->safeDataset('Current bed occupancy', function () use ($repo) {
            return $repo->currentOccupancy();
        }, [], $warnings, 'Beds');

        $annual = $this->safeDataset('Annual bed occupancy hours', function () use ($repo, $year, $compareYear) {
            return $repo->annualSummary($year, $compareYear);
        }, [], $warnings, 'Beds');

        $units = $this->safeDataset('Unit occupancy', function () use ($repo, $year) {
            return $repo->unitOccupancy($year, 12);
        }, [], $warnings, 'Beds');

        $topBeds = $this->safeDataset('Top beds occupancy', function () use ($repo, $year) {
            return $repo->topBeds($year, 15);
        }, [], $warnings, 'Beds');

        $current = is_array($current) ? $current : [];
        $annual = is_array($annual) ? $annual : [];
        $units = is_array($units) ? $units : [];
        $topBeds = is_array($topBeds) ? $topBeds : [];

        $cards = [
            'occupancy' => [
                'title' => \t('beds.card_occupancy'),
                'current_total' => (float)($current['occupancy_rate'] ?? 0),
                'compare_total' => (float)($annual['compare_efficiency'] ?? 0),
                'percent' => null,
                'hint' => \t('beds.current_rate_hint'),
                'mode' => 'percent',
            ],
            'occupied' => [
                'title' => \t('beds.card_occupied'),
                'current_total' => (float)($current['occupied_beds'] ?? 0),
                'compare_total' => (float)($current['total_beds'] ?? 0),
                'percent' => null,
                'hint' => \t('beds.total_beds_hint', ['total' => number_format((float)($current['total_beds'] ?? 0), 0)]),
                'mode' => 'single',
            ],
            'available' => [
                'title' => \t('beds.card_available'),
                'current_total' => (float)($current['available_beds'] ?? 0),
                'compare_total' => 0.0,
                'percent' => null,
                'hint' => \t('beds.available_hint'),
                'mode' => 'single',
            ],
            'hours' => $this->doctorMetricCard(\t('beds.card_hours'), (float)($annual['current_hours'] ?? 0), (float)($annual['compare_hours'] ?? 0), $year, $compareYear, $chartService),
        ];

        return [
            'current' => $current,
            'annual' => $annual,
            'cards' => $cards,
            'units' => $units,
            'topBeds' => $topBeds,
            'availableYears' => $availableYears,
            'warnings' => $warnings,
            'unitsPayload' => $this->singleSeriesPayload(\t('beds.unit_chart_title'), $units, \t('beds.occupancy_percent')),
            'topBedsPayload' => $this->singleSeriesPayload(\t('beds.top_beds_title'), $topBeds, \t('beds.occupancy_percent')),
        ];
    }

    /**
     * @param callable(): mixed $callback
     * @param mixed $fallback
     * @param array<int, string> $warnings
     * @return mixed
     */
    private function safeDataset(string $label, callable $callback, $fallback, array &$warnings, string $scope)
    {
        try {
            return $callback();
        } catch (Throwable $e) {
            $warnings[] = $label . ': ' . $e->getMessage();
            $this->logWorkspaceError($scope . ' dataset warning [' . $label . ']', $e);
            return $fallback;
        }
    }

    private function logWorkspaceError(string $label, Throwable $e): void
    {
        $logDir = \storage_path('logs');
        if (!is_dir($logDir)) {
            @mkdir($logDir, 0775, true);
        }
        @error_log('[' . date('c') . '] ' . $label . ': ' . $e . PHP_EOL, 3, $logDir . '/app.log');
    }

    private function metricCard(string $title, array $rows, int $year, int $compareYear, ChartDataService $chartService): array
    {
        $current = $chartService->total($rows, 'current_value');
        $compare = $chartService->total($rows, 'compare_value');
        return [
            'title' => $title,
            'current_total' => $current,
            'compare_total' => $compare,
            'percent' => $chartService->percentChange($current, $compare),
            'hint' => $year . ' ' . \t('visits.vs') . ' ' . $compareYear,
            'mode' => 'compare',
        ];
    }

    private function metricCardForPeriod(
        string $title,
        array $rows,
        string $currentPeriod,
        string $comparePeriod,
        ChartDataService $chartService
    ): array {
        $current = $chartService->total($rows, 'current_value');
        $compare = $chartService->total($rows, 'compare_value');
        return [
            'title' => $title,
            'current_total' => $current,
            'compare_total' => $compare,
            'percent' => $chartService->percentChange($current, $compare),
            'hint' => $currentPeriod . ' ' . \t('visits.vs') . ' ' . $comparePeriod,
            'mode' => 'compare',
        ];
    }

    private function doctorMetricCard(string $title, float $current, float $compare, int $year, int $compareYear, ChartDataService $chartService): array
    {
        return [
            'title' => $title,
            'current_total' => $current,
            'compare_total' => $compare,
            'percent' => $chartService->percentChange($current, $compare),
            'hint' => $year . ' ' . \t('visits.vs') . ' ' . $compareYear,
            'mode' => 'compare',
        ];
    }

    private function followupSummary(array $rows): array
    {
        $new = 0.0;
        $followup = 0.0;
        foreach ($rows as $row) {
            $category = strtolower((string)($row['category'] ?? ''));
            if ($category === 'followup') {
                $followup += (float)($row['value'] ?? 0);
            } else {
                $new += (float)($row['value'] ?? 0);
            }
        }
        $total = $new + $followup;
        return [
            'new' => $new,
            'followup' => $followup,
            'total' => $total,
            'ratio' => $total > 0 ? ($followup / $total) * 100 : null,
            'new_ratio' => $total > 0 ? ($new / $total) * 100 : null,
        ];
    }

    /**
     * @param array<int, array<string, mixed>> $rows
     * @return array<int, array{category:string,value:float,percent:float}>
     */
    private function normalizeAdmissionCategories(array $rows): array
    {
        $total = 0.0;
        foreach ($rows as $row) {
            $total += (float)($row['value'] ?? 0);
        }

        return array_map(static function (array $row) use ($total): array {
            $value = (float)($row['value'] ?? 0);
            return [
                'category' => strtolower(trim((string)($row['category'] ?? 'other'))),
                'value' => $value,
                'percent' => $total > 0 ? ($value / $total) * 100 : 0.0,
            ];
        }, $rows);
    }

    private function normalizeDailyRows(array $rows): array
    {
        return array_map(function (array $row): array {
            return [
                'label' => trim((string)($row['day_label'] ?? '')),
                'value' => (float)($row['value'] ?? 0),
            ];
        }, $rows);
    }

    private function normalizeSpecialties(array $rows): array
    {
        return array_map(function (array $row): array {
            return [
                'label' => trim((string)($row['specialty_name'] ?? $row['label'] ?? '')),
                'value' => (float)($row['value'] ?? 0),
            ];
        }, $rows);
    }

    /** @return array<int, array{label:string,value:float}> */
    private function normalizeOperationRanks(array $rows, string $labelKey): array
    {
        return array_map(static function (array $row) use ($labelKey): array {
            return [
                'label' => trim((string)($row[$labelKey] ?? $row['label'] ?? '')),
                'value' => (float)($row['value'] ?? 0),
            ];
        }, $rows);
    }

    private function metricLabel(array $availableMetrics, string $metric): string
    {
        foreach ($availableMetrics as $row) {
            if (($row['key'] ?? '') === $metric) {
                return \t((string)$row['label_key']);
            }
        }
        return \t('doctors.metric_revenue');
    }

    private function singleSeriesPayload(string $title, array $rows, string $label): array
    {
        return [
            'title' => $title,
            'labels' => array_map(function (array $row): string {
                return mb_substr(trim((string)($row['label'] ?? '')), 0, 24);
            }, $rows),
            'series' => [[
                'label' => $label,
                'values' => array_map(function (array $row): float {
                    return (float)($row['value'] ?? 0);
                }, $rows),
            ]],
        ];
    }
}
